-
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
Changes from 12 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,6 +1091,7 @@ Server.prototype._onHandlerError = function _onHandlerError(err, req, res) { | |
return; | ||
} | ||
|
||
// Handlers don't continue when error happen | ||
res._handlersFinished = true; | ||
|
||
// Preserve handler err for finish event | ||
|
@@ -1138,27 +1139,31 @@ 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. | ||
// We don't set _connectionState to "close" in the happy path | ||
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; | ||
res.err = err; | ||
} | ||
req.once('close', onClose); | ||
|
||
// Response lifecycle events | ||
function onFinish() { | ||
function onResFinish() { | ||
var processHrTime = process.hrtime(); | ||
|
||
res._flushed = true; | ||
|
@@ -1170,22 +1175,28 @@ 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 | ||
// In Node < 10 "close" event dont fire always | ||
// https://github.com/nodejs/node/pull/20611 | ||
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) { | ||
|
||
// We are handling when connection is being closed prematurely outside of | ||
// restify. It's not because the req is aborted. | ||
function onResClose() { | ||
res._flushed = true; | ||
req._timeFlushed = process.hrtime(); | ||
|
||
req.removeListener('close', onClose); | ||
res.removeListener('finish', onFinish); | ||
self._finishReqResCycle(req, res, err); | ||
// Finish may already set the req._timeFlushed | ||
req._timeFlushed = req._timeFlushed || process.hrtime(); | ||
|
||
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 +1267,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 |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
|
||
// external requires | ||
var assert = require('chai').assert; | ||
|
||
var restify = require('../../lib/index.js'); | ||
var restifyClients = require('restify-clients'); | ||
|
||
|
@@ -27,8 +28,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,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 commentThe reason will be displayed to describe this comment to others. Learn more. Is that comment still relevant? |
||
assert.equal(metrics.connectionState, undefined); | ||
assert.equal(metrics.method, 'GET'); | ||
assert.isNumber(metrics.inflightRequests); | ||
|
@@ -103,6 +104,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 +141,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 +245,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 +269,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,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 commentThe reason will be displayed to describe this comment to others. Learn more. Is that comment still relevant? |
||
assert.equal(metrics.connectionState, undefined); | ||
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.
Why do we need to set
_sent
totrue
?