Skip to content
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

feat: provide callback to uncaughtException handler #1766

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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() {
Copy link
Member

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
Member

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
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