-
Notifications
You must be signed in to change notification settings - Fork 982
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(server): address Node v10.x req close event firing order #1672
Conversation
Changes introduced in: nodejs/node#20941 nodejs/node#20611
lib/server.js
Outdated
req._connectionState = 'close'; | ||
|
||
// Do no handle close if request is already flushed |
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.
Typo "Do not"?
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.
Also, is it "if response is already flushed" here, or do we mean "request" in the sense of "the whole request/response"? I think it'd help to distinguish between request and response here for clarity, and in this case it seems we're checking if the response is flushed.
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.
good catch!
lib/server.js
Outdated
req._connectionState = 'close'; | ||
|
||
// Do no handle close if request is already flushed | ||
// In Node >= 10 res's close event is always emitted |
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.
Is is Node > 10.4.0 at least? It seems that change hasn't been made it to any release yet.
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 fix seems to be working with all supported Node.js versions, 6,8 10.0.0 and 10.4.0 as well.
I made the comment more accurate.
lib/server.js
Outdated
// Handle res's close on to the next cycle | ||
// Required in Node >= 10: | ||
// introduced in: https://github.com/nodejs/node/pull/20941 | ||
req.once('close', function onReqClose() { |
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.
Is it that because res' 'close'
event can now be emitted using process.nextTick
we want to handle req's 'close'
event on the actual next tick of the libuv event loop so that we process req's 'close'
event after res
's 'close'
event?
It seems this ordering could break if .e.g node started emitting the res' 'close'
event using setImmediate
, which seems a bit brittle.
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.
Yes, what would you recommend to make it more robust?
res.send('hello world'); | ||
setTimeout(function() { | ||
next(); | ||
}, 500); |
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.
Why do we need any setTimeout
call here?
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.
We generate some "work" in the server to be sure that events are firing in the right order. In my previous fix tests passed with positive false.
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.
What do you mean by "some work"? It seems it's not CPU-bound "work", so is it a way to make events on request/response objects to happen in a specific order or within a certain timeframe? If so, should we document what the test is actually testing?
EDIT: I get it now: we want to send the response and call the next
callback on separate turns of the event loop to exercise a specific edge case. Following question + whether we should document what we're doing with this test still relevant:
Why not keeping the original test? Should be add a new test instead of replacing the existing one?
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.
Remaining questions above still relevant:
- Should we document why we want to control the timing of those operations in this test?
- Should we also keep the original test and add a new test with those changes?
test/plugins/metrics.test.js
Outdated
assert.equal(metrics.connectionState, undefined); | ||
// close in Node >= 10 | ||
assert.include( | ||
[undefined, 'close'], |
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.
What does that '[undefined, 'close']
mean?
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.
Any of them. Undefined is the unknown connection state basically.
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.
What are the cases when the connection state would be 'close'
? Shouldn't it be 'close'
only when the request was aborted? In this case it doesn't seem like the request would be aborted.
lib/server.js
Outdated
res.once('error', onError); | ||
|
||
// Request events | ||
req.once('close', onReqClose); |
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.
The new logic seems a lot more robust than the original changes 👍
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 also ironic that the Node.js documentation states that after the 'finish'
event, no more events will be emitted on the response object, when it's clearly not true after the change at nodejs/node#20611.
In general it seems that we should not consider that a For instance, such an event will always be emitted when the server finishes writing on the response stream, regardless of the state of the request. That So instead of using the It might allow to simplify the current changes even further. |
Excellent idea, implemented! |
lib/server.js
Outdated
res._flushed = true; | ||
req._timeFlushed = process.hrtime(); | ||
// Finish may alread set flushed timer |
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.
nit: typo "already".
lib/server.js
Outdated
@@ -1091,7 +1091,8 @@ Server.prototype._onHandlerError = function _onHandlerError(err, req, res) { | |||
return; | |||
} | |||
|
|||
res._handlersFinished = true; | |||
// Handlers doesn't continue when error happen | |||
self._onHandlerStop(req, res); |
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.
What are the implications of calling _onHandlerStop
instead of "just" setting _handlersFinished = true
?
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.
reverted
lib/server.js
Outdated
self._finishReqResCycle(req, res); | ||
} | ||
function onError(err) { | ||
|
||
// Emitted when the response has been sent. |
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.
Is this still true? Also, I think I know the answer, but is it possible for close to get fired but _flushed
is false?
lib/server.js
Outdated
req.removeListener('close', onClose); | ||
res.removeListener('error', onError); | ||
|
||
// Do not trigger false | ||
self._finishReqResCycle(req, res); |
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.
Why do we need to call _finishReqResCycle
here?
lib/server.js
Outdated
req.removeListener('close', onClose); | ||
res.removeListener('finish', onFinish); | ||
self._finishReqResCycle(req, res, err); | ||
self._finishReqResCycle(req, res, res.err); |
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.
Why do we need to call _finishReqResCycle
here?
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.
To tigger after (after is triggered after both handlers are finished and req is flushed)
@@ -103,6 +106,7 @@ describe('request metrics plugin', function() { | |||
it('should return metrics with pre error', function(done) { | |||
SERVER.on('uncaughtException', function(req, res, route, err) { | |||
assert.ok(err); | |||
res.send(err); |
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.
Is there any exception thrown in this test? It doesn't seem so, it seems there's only an error being passed to a handler's next
callback? If so, why does the server emit 'uncaughtException'
?
Also, why do we need to res.send()
now when we wouldn't need to do it before?
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.
f you listen to this event, you must send a response to the client. This behavior is different from the standard error events. If you do not listen to this event, restify’s default behavior is to call res.send() with the error that was thrown. - docs
So it should have been there
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.
So in this test before your changes we would call done()
because the 'after'
event would be emitted no matter what, but we wouldn't execute the assertions in the client's request's callback?
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.
yes
@@ -139,6 +143,7 @@ describe('request metrics plugin', function() { | |||
it('should return metrics with use error', function(done) { | |||
SERVER.on('uncaughtException', function(req, res, route, err) { | |||
assert.ok(err); | |||
res.send(err); |
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.
Same question regarding 'uncaughtException'
and the need for res.send()
than above.
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.
see comment above
test/plugins/metrics.test.js
Outdated
assert.equal(metrics.connectionState, undefined); | ||
// close in Node >= 10 | ||
assert.include( | ||
[undefined, 'close'], |
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.
Why is there two possible values here? Shouldn't we always expect 'close'
?
@@ -260,6 +271,7 @@ describe('request metrics plugin', function() { | |||
{ | |||
server: SERVER | |||
}, | |||
// TODO: test timeouts if any of the following asserts fails |
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.
Should we file a corresponding issue?
@hekike Added a bunch more comments, but I think this is going in the right direction, thank you very much again for considering my feedback! |
test/plugins/metrics.test.js
Outdated
assert.equal(metrics.connectionState, undefined); | ||
// close doesn't always fire in Node < 10 | ||
assert.include( | ||
[undefined, 'close'], |
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.
The problem with using assert.include
here is that if we run this test with Node.js >= 10.x and we expect the connection state to be 'close'
, the test would pass even if the connection state was undefined
. Would it be too much work to differentiate between node >= 10.x and < 10.x here and have two different asserts, something like:
if (process.version >= 10.0) {
assert.equal(metrics.connectionState, 'close');
} else {
assert.equal(metrics.connectionState, undefined);
}
?
Same for all other connection state assertions in this file.
test/plugins/metrics.test.js
Outdated
@@ -63,6 +63,7 @@ describe('request metrics plugin', function() { | |||
assert.isAtLeast(metrics.latency, 150); | |||
assert.isAtLeast(metrics.totalLatency, 150); | |||
assert.equal(metrics.path, '/foo'); | |||
// close doesn't always fire in Node < 10 |
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.
Is that comment still relevant?
test/plugins/metrics.test.js
Outdated
@@ -271,6 +281,7 @@ describe('request metrics plugin', function() { | |||
assert.isNumber(metrics.latency, 200); | |||
assert.equal(metrics.path, '/foo'); | |||
assert.equal(metrics.method, 'GET'); | |||
// close doesn't always fire in Node < 10 |
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.
Is that comment still relevant?
lib/server.js
Outdated
// Set status code and _sent for audit | ||
// as req is already closed connection | ||
res.statusCode = err.statusCode; | ||
res._sent = true; |
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.
Why do we need to set _sent
to true
?
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.
Beautiful! Thanks for being persistent and patient with me!
Thanks for re-reviewing, restify just get better ;) |
Req close always fires in Node 10 and fires in a process.nextTick
This PR handles the close event in restify in a way that we only return an error if the response is not flushed yet.
Related changes in Node: