Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions docs/api/server-events.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put a comment about the why here and describe what will happen with and without domains.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hekike @rajatkumar Added some comments here, let me know if that addresses your feedback!

// 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
Original file line number Diff line number Diff line change
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