-
-
Notifications
You must be signed in to change notification settings - Fork 130
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(MockHttpSocket): handle response stream errors #548
fix(MockHttpSocket): handle response stream errors #548
Conversation
597d301
to
3510477
Compare
Alright, so treating those response stream errors as request errors isn't right. Even the original comment says so. The problem is, I cannot reproduce the function createErrorStream() {
return new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('hello'))
controller.error(new Error('stream error'))
},
})
}
const httpServer = new HttpServer((app) => {
app.get('/resource', (req, res) => {
res.pipe(Readable.fromWeb(createErrorStream()))
})
}) const request = http.get(httpServer.http.url('/resource'))
request.on('error', requestErrorListener)
request.on('response', (response) => {
console.log('RESPONSE!')
response.on('error', (error) => {
console.log('ERROR!', error)
})
}) The @mikicho, is this roughly how you are reproducing this in a non-mocked scenario? |
// This way, the client still receives the "response" event, | ||
// and the actual stream error is forwarded as the "error" | ||
// event on the http.IncomingMessage instance. | ||
flushHeaders() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will emit the response
event on the client because it will receive the response headers.
|
||
// Forward response streaming errors as response errors. | ||
/** @todo This doesn't quite do it. */ | ||
// this.destroy(error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs help
I'm struggling to find a way to trigger this:
The error
event on http.IncomingMessage
is emitted when the socket is destroyed (implemented in the ._destroy()
method). For some reason, calling this.destroy()
on the HttpMockSocket
doesn't trigger that.
if (httpHeaders.length > 0) { | ||
flushHeaders() | ||
} | ||
flushHeaders() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's safe to call flushHeaders()
because it will do nothing if the headers have already been flushed.
How this works in non-mocked worldWhen there's a response stream error, the server throws an error and, usually, handles it as a 500 response:
This means that erroring the response stream just results in an error thrown. It doesn't get forwarded to ClientRequest or IncomingMessage in any special way, aside from a 500 response arriving to the client. @mikicho, is this the same expectation Nock has in this scenario? |
This should be fixed by #532 but this PR still contains viable tests and the fix (flushing response headers on unhandled response stream errors). |
@mikicho, I think this is done! Response stream errors are now handled as 500 error responses. I believe that's what's happening in the actual server as well. |
} | ||
} catch (error) { | ||
if (error instanceof Error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a blocker, but I think we need to return 500 for any error.
The current implementation swallows the error from the user in case it does not extend Error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I completely agree! I revisited by previous point and found it wrong. I'm alining the uncaught exceptions handling in #555, and then will update this branch to migrate the implementation from NodeClientRequest.ts
.
No, Nock propagates the error event to the response ( I think Nock's behavior provides better DX and is more predictable from the user's POV. |
While Nock compatibility is the goal, we should focus on how Node.js behaves here, not Nock. From everything I've tried so far, Node doesn't handle response stream errors in any special way. Errors via When piping a regular |
One more argument to preserving non-error exceptions is you can throw a This is an intentional design to support Remix-like response throwing. I've added this intention as automated tests in #553. |
// Coerce response stream errors to 500 responses. | ||
// Don't flush the original response headers because | ||
// unhandled errors translate to 500 error responses forcefully. | ||
this.respondWith(createServerErrorResponse(error)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Treating all the response stream errors as 500 error responses! Consistency.
I will merge this one. If I missed anything, please let me know, we can iterate on it in the follow-up pull request. Thanks! |
085d1ec
into
feat/yet-another-socket-interceptor
@kettanaito I don't think it is not a question of compatibility. The scenario, as I see it, is in-process Readable failure, not server-side failure, and in Node.js, probably extremely rare (I can't even think of such a case) This is why I believe the current behavior is not quite right and could be causing some confusion. We take a user-side error and convert it to 500 error although the "server" was ok. http.get(..., res => {
res.on('error', () => {}
} |
But that's the point, it wasn't. When mocking, your request listener is the server. If there's an exception there, it's equivalent to the actual server getting an exception on its runtime, which is often handled as a 500 error response. Note that this has no effect on the bypassed requests.
Technically, when the response stream is destroyed. That's the only occasion per spec when Here's the only scenario where I think the error will be emitted correctly: http.get(url, (res) => {
res.on('error', () => console.log('Error!'))
res.destroy(new Error('Do not need this anymore'))
}) Destroying the IncomingMessage stream directly will emit the error event. But since you don't have access to the IncomingMessage in the request listener, you can't destroy the response that way. |
While this is true, in this case, I see the error as coming from Node.js itself, not from the server. I think it would provide better DX. |
When a mocked response stream receives an error (e.g.
controller.error(error)
), translate that to request errors (i.e. emit the "error" event and abort the request).Todo