-
Notifications
You must be signed in to change notification settings - Fork 984
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
Changes from 7 commits
929f9a1
b237d3e
99bc0bf
c54c3c1
5996573
245a91d
f82c3a6
b84caa8
4e8e5de
6cb9fc7
ee3b338
df356c9
10410fb
def6164
3d0bd42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
|
||
// Preserve handler err for finish event | ||
res.err = res.err || err; | ||
|
@@ -1138,27 +1139,29 @@ Server.prototype._setupRequest = function _setupRequest(req, res) { | |
} | ||
|
||
// Request lifecycle events | ||
// attach a listener for 'close' events, this will let us set | ||
// a flag so that we can stop processing the request if the client closes | ||
// attach a listener for 'aborted' events, this will let us set | ||
// a flag so that we can stop processing the request if the client aborts | ||
// the connection (or we lose the connection). | ||
// we consider a closed request as flushed from metrics point of view | ||
function onClose() { | ||
res._flushed = true; | ||
req._timeFlushed = process.hrtime(); | ||
|
||
res.removeListener('finish', onFinish); | ||
res.removeListener('error', onError); | ||
function onReqAborted() { | ||
// Request was aborted, override the status code | ||
var err = new errors.RequestCloseError(); | ||
err.statusCode = 444; | ||
|
||
// For backward compatibility we only set connection state to "close" | ||
// for RequestCloseError, also aborted is always immediatly followed | ||
// by a "close" event | ||
req._connectionState = 'close'; | ||
|
||
// Request was aborted or closed. Override the status code | ||
res.statusCode = 444; | ||
|
||
self._finishReqResCycle(req, res, new errors.RequestCloseError()); | ||
// 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 commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to set |
||
res.err = err; | ||
} | ||
req.once('close', onClose); | ||
|
||
// Response lifecycle events | ||
function onFinish() { | ||
function onResFinish() { | ||
var processHrTime = process.hrtime(); | ||
|
||
res._flushed = true; | ||
|
@@ -1170,22 +1173,24 @@ Server.prototype._setupRequest = function _setupRequest(req, res) { | |
req._timeUseEnd = req._timeUseEnd || processHrTime; | ||
req._timeRouteEnd = req._timeRouteEnd || processHrTime; | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to call |
||
} | ||
function onError(err) { | ||
|
||
// Emitted when the response has been sent. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
function onResClose() { | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. nit: typo "already". |
||
req._timeFlushed = req._timeFlushed || process.hrtime(); | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to call There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
||
} | ||
res.once('finish', onFinish); | ||
res.once('error', onError); | ||
|
||
// Request events | ||
req.once('aborted', onReqAborted); | ||
|
||
// Response events | ||
res.once('finish', onResFinish); | ||
res.once('close', onResClose); | ||
|
||
// attach a listener for the response's 'redirect' event | ||
res.on('redirect', function onRedirect(redirectLocation) { | ||
|
@@ -1256,7 +1261,6 @@ Server.prototype._routeErrorResponse = function _routeErrorResponse( | |
) { | ||
var self = this; | ||
|
||
// if (self.handleUncaughtExceptions) { | ||
if (self.listenerCount('uncaughtException') > 1) { | ||
self.emit('uncaughtException', req, res, req.route, err); | ||
return; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,8 +27,7 @@ describe('request metrics plugin', function() { | |
CLIENT = restifyClients.createJsonClient({ | ||
url: 'http://127.0.0.1:' + PORT, | ||
dtrace: helper.dtrace, | ||
retry: false, | ||
requestTimeout: 200 | ||
retry: false | ||
}); | ||
|
||
done(); | ||
|
@@ -63,7 +62,11 @@ describe('request metrics plugin', function() { | |
assert.isAtLeast(metrics.latency, 150); | ||
assert.isAtLeast(metrics.totalLatency, 150); | ||
assert.equal(metrics.path, '/foo'); | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. What does that There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. What are the cases when the connection state would be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem with using
? Same for all other connection state assertions in this file. |
||
metrics.connectionState | ||
); | ||
assert.equal(metrics.method, 'GET'); | ||
assert.isNumber(metrics.inflightRequests); | ||
|
||
|
@@ -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 commentThe 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 Also, why do we need to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So it should have been there There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So in this test before your changes we would call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
}); | ||
|
||
SERVER.on( | ||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. Same question regarding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see comment above |
||
}); | ||
|
||
SERVER.on( | ||
|
@@ -242,11 +247,17 @@ describe('request metrics plugin', function() { | |
} | ||
); | ||
|
||
CLIENT.get('/foo?a=1', function(err, _, res) { | ||
// request should timeout | ||
assert.ok(err); | ||
assert.equal(err.name, 'RequestTimeoutError'); | ||
}); | ||
CLIENT.get( | ||
{ | ||
path: '/foo?a=1', | ||
requestTimeout: 200 | ||
}, | ||
function(err, _, res) { | ||
// request should timeout | ||
assert.ok(err); | ||
assert.equal(err.name, 'RequestTimeoutError'); | ||
} | ||
); | ||
}); | ||
|
||
it('should handle uncaught exceptions', function(done) { | ||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. Should we file a corresponding issue? |
||
function(err, metrics, req, res, route) { | ||
assert.ok(err); | ||
assert.equal(err.name, 'Error'); | ||
|
@@ -271,7 +283,11 @@ describe('request metrics plugin', function() { | |
assert.isNumber(metrics.latency, 200); | ||
assert.equal(metrics.path, '/foo'); | ||
assert.equal(metrics.method, 'GET'); | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Why is there two possible values here? Shouldn't we always expect |
||
metrics.connectionState | ||
); | ||
assert.isNumber(metrics.inflightRequests); | ||
return done(); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1924,8 +1924,12 @@ test("should emit 'after' on successful request", function(t) { | |
}); | ||
|
||
SERVER.get('/foobar', function(req, res, next) { | ||
res.send('hello world'); | ||
next(); | ||
setTimeout(function() { | ||
res.send('hello world'); | ||
setTimeout(function() { | ||
next(); | ||
}, 500); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need any There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more.
EDIT: I get it now: we want to send the response and call the 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 commentThe reason will be displayed to describe this comment to others. Learn more. Remaining questions above still relevant:
|
||
}, 500); | ||
}); | ||
|
||
CLIENT.get('/foobar', function(err, _, res) { | ||
|
@@ -1995,6 +1999,7 @@ test( | |
SERVER.on('after', function(req, res, route, err) { | ||
t.ok(err); | ||
t.equal(req.connectionState(), 'close'); | ||
t.equal(res.statusCode, 444); | ||
t.equal(err.name, 'RequestCloseError'); | ||
t.end(); | ||
}); | ||
|
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