Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ClientRequest): use ServerResponse to build the HTTP response string #596

Merged
merged 5 commits into from
Jul 6, 2024

Conversation

mikicho
Copy link
Contributor

@mikicho mikicho commented Jul 6, 2024

Context: this allows us to support mocked responses with Transfer-Encoding and, potentially, other response transformations.

I still need to give meaningful names to some variables, but it works as expected.

The idea here is that MSW acts as a Node.js server and uses the ServerResponse class to build the final chunks that we later push to our socket.

The only caveat I found is that we are maybe TOO close to the Node.js implementation 😅. For example, Nock's tests found that ServerResponse automatically returns connection and date headers, which we now add to the response.
I think we can remove this default behavior, but I first want to know how you see it.
@kettanaito

callback()
},
})
const fakeResponse = new ServerResponse(new IncomingMessage(new net.Socket()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance this can also handle compression for us?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. good point.
I thought we would get this OOTB from Node.js when the response goes back to the client.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope so! I think we have some tests for compression but I got a new issue about it in MSW recently, which made me think something was still missing. Anyways, we'll see.

const httpHeaders: Array<Buffer> = []
// Create a `ServerResponse` instance to delegate HTTP message parsing,
// Transfer-Encoding, and other things to Node.js internals.
const serverResponse = new ServerResponse(new IncomingMessage(this))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I passed this socket instance as the socket of the IncomingMessage used here. I think it should be more error-proof in case this server response decides to terminate the socket (rightfully), for example.

Copy link
Contributor Author

@mikicho mikicho Jul 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used a new socket because I was afraid of infinity loops, but if it makes sense to you, I'm ok with this

Copy link
Member

@kettanaito kettanaito Jul 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't arrive at the infinite loops. This logic is only triggered on respondWith(), and that's called by the developer. If I spot any issues with this, will revert back to use an empty socket.

* @see https://github.com/nodejs/node/blob/10099bb3f7fd97bb9dd9667188426866b3098e07/test/parallel/test-http-server-response-standalone.js#L32
*/
serverResponse.assignSocket(
new MockSocket({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the MockSocket we already have instead of Writable. While sockets extend streams, they implement additional methods on top. I know Node.js uses a simple Writable in their test but we have to acknowledge that test case is controlled. Our use cases are going to be everything out there. Using an actual socket-compatible instance is crucial here.

// An empty line separating headers from the body.
httpHeaders.push(Buffer.from('\r\n'))

const flushHeaders = (value?: Uint8Array) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like that we can drop this now. Fantastic!

const { res, text } = await waitForClientRequest(request)

expect(res.statusCode).toBe(200)
expect(res.headers).toHaveProperty('transfer-encoding', 'chunked')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a few assertions that the client also receives the right response headers.

Copy link
Member

@kettanaito kettanaito left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nothing short of fantastic. Great job, @mikicho 👏

@kettanaito
Copy link
Member

kettanaito commented Jul 6, 2024

Regarding the Connection and Date headers being added to the mocked response, I'm not sure. I'm leaning toward removing them. Only the headers the developer themselves provided in the mocked response instance should be set.

I've noticed we don't have any tests for strict response header equality either. Perhaps this is the time we added them.

@mikicho, what do you think about this?

@kettanaito
Copy link
Member

From the RFC9110, the Date section:

An origin server with a clock (as defined in Section 5.6.7) MUST generate a Date header field ...
An origin server without a clock MUST NOT generate a Date header field.

So the addition of the Date (and I assume by extension the Connection header) seems to be dictated by the specification. Node.js just follows it.

Now, it's debatable who should be in charge of this. I think it's may be okay for us to leave those headers if they mean better spec compatibility out of the box. It'd even be good, in fact. As long as we are sure that the developer can override (or remove) those headers if they want to.

@kettanaito kettanaito changed the title use ServerResponse to build the HTTP response string fix(ClientRequest): use ServerResponse to build the HTTP response string Jul 6, 2024
@kettanaito
Copy link
Member

My opinion on this jumps back and forth every time I think about it.

The connection and date headers make sense from the server's perspective. Request listeners are not technically servers. You can emulate one if you want, but you are not required to.

Moreover, new Response() doesn't attach those headers by default. It may be confusing to see them added by the interceptor when the developer had no intention of having them added.

Let's remove those server headers. Keep the existing behavior, make the mocked response a WYSIWYG.

@mikicho
Copy link
Contributor Author

mikicho commented Jul 6, 2024

I'm leaning toward removing them.

Me, too. I think we are a mocking tool and the DX > compatibility. It may be confusing because, as a mocking tool, compatibility IS better than DX (it prevents terrible production errors).
Anyway, in this specific case, I think we should remove these default headers.

@kettanaito
Copy link
Member

@mikicho, agreed. Pushed the fix and also adjusted the test for the empty mocked response to assert that the received IncomingMessage headers are empty.

)
serverResponse.statusCode = response.status
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing these server response headers before the developer-sent headers. They can always set these if they wish to.

@kettanaito kettanaito merged commit fedac45 into main Jul 6, 2024
2 checks passed
@kettanaito kettanaito deleted the Michael/nodejs-support-transfer-encoding branch July 6, 2024 11:11
@kettanaito kettanaito mentioned this pull request Jul 6, 2024
19 tasks
@kettanaito
Copy link
Member

Released: v0.32.1 🎉

This has been released in v0.32.1!

Make sure to always update to the latest version (npm i @mswjs/interceptors@latest) to get the newest features and bug fixes.


Predictable release automation by @ossjs/release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants