Skip to content

Commit

Permalink
feat: provide callback to uncaughtException handler (#1766)
Browse files Browse the repository at this point in the history
  • Loading branch information
cprussin authored and DonutEspresso committed Apr 11, 2019
1 parent f363b36 commit 5e8b5e2
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 5 deletions.
14 changes: 10 additions & 4 deletions docs/api/server-events.md
Expand Up @@ -107,15 +107,21 @@ server.get('/', function(req, res, next) {
return next();
});

server.on('uncaughtException', function(req, res, route, err) {
server.on('uncaughtException', function(req, res, route, err, callback) {
// this event will be fired, with the error object from above:
// ReferenceError: x is not defined
res.send(504, 'boom');
callback();
});
```

If 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
If you listen to this event, you __must__:

1. send a response to the client _and_
2. call the callback function passed as the fourth argument of the event listener

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.

The signature is for the after event is as follows:
Expand Down
21 changes: 20 additions & 1 deletion lib/server.js
Expand Up @@ -1405,7 +1405,26 @@ Server.prototype._routeErrorResponse = function _routeErrorResponse(
self.handleUncaughtExceptions &&
self.listenerCount('uncaughtException') > 1
) {
self.emit('uncaughtException', req, res, req.route, err);
self.emit(
'uncaughtException',
req,
res,
req.route,
err,
function uncaughtExceptionCompleted() {
// We provide a callback to listeners of the 'uncaughtException'
// event and we call _finishReqResCycle when that callback is
// called so that, in case the actual request/response lifecycle
// was completed _before_ the error was thrown or emitted, and
// thus _before_ route handlers were marked as "finished", we
// can still mark the req/res lifecycle as complete.
// This edge case can occur when e.g. a client aborts a request
// and the route handler that handles that request throws an
// uncaught exception _after_ the request was aborted and the
// response was closed.
self._finishReqResCycle(req, res, err);
}
);
return;
}

Expand Down
45 changes: 45 additions & 0 deletions test/server.test.js
Expand Up @@ -2051,6 +2051,51 @@ test(
}
);

// This test reproduces https://github.com/restify/node-restify/issues/1765. It
// specifically tests the edge case of an exception being thrown from a route
// handler _after_ the response is considered to be "flushed" (for instance when
// the request is aborted before a response is sent and an exception is thrown).
// eslint-disable-next-line max-len
test("should emit 'after' on uncaughtException after response closed with custom uncaughtException listener", function(t) {
var ERR_MSG = 'foo';
var gotAfter = false;
var gotReqCallback = false;

SERVER.on('after', function(req, res, route, err) {
gotAfter = true;
t.ok(err);
t.equal(req.connectionState(), 'close');
t.equal(res.statusCode, 444);
t.equal(err.name, 'Error');
t.equal(err.message, ERR_MSG);
if (gotReqCallback) {
t.end();
}
});

SERVER.on('uncaughtException', function(req, res, route, err, callback) {
callback();
});

SERVER.get('/foobar', function(req, res, next) {
res.on('close', function onResClose() {
// We throw this error in the response's close event handler on
// purpose to exercise the code path where we mark the route
// handlers as finished _after_ the response is marked as flushed.
throw new Error(ERR_MSG);
});
});

FAST_CLIENT.get('/foobar', function(err, _, res) {
gotReqCallback = true;
t.ok(err);
t.equal(err.name, 'RequestTimeoutError');
if (gotAfter) {
t.end();
}
});
});

test('should increment/decrement inflight request count', function(t) {
SERVER.get('/foo', function(req, res, next) {
t.equal(SERVER.inflightRequests(), 1);
Expand Down

0 comments on commit 5e8b5e2

Please sign in to comment.