From 4900d6bdd51fa4e1769678562de69929c38a0c4b Mon Sep 17 00:00:00 2001 From: Peter Marton Date: Fri, 18 Jan 2019 16:19:16 -0800 Subject: [PATCH] feat(req): add restifyDone event (#1740) --- docs/_api/request.md | 136 +++++++++++++++++++++++++++++++++++++ docs/api/request-events.md | 21 ++++++ docs/config/request.yaml | 2 + lib/server.js | 4 +- test/request.test.js | 52 ++++++++++++++ 5 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 docs/api/request-events.md diff --git a/docs/_api/request.md b/docs/_api/request.md index f7ad56dec..c3d65f5e2 100644 --- a/docs/_api/request.md +++ b/docs/_api/request.md @@ -33,6 +33,7 @@ permalink: /docs/request-api/ - [endHandlerTimer](#endhandlertimer) - [connectionState](#connectionstate) - [getRoute](#getroute) +- [Events](#events) - [Log](#log) ## Request @@ -378,6 +379,141 @@ _Route info object structure:_ Returns **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** route +## Events + +In additional to emitting all the events from node's +[http.Server](http://nodejs.org/docs/latest/api/http.html#http_class_http_server), +restify servers also emit a number of additional events that make building REST +and web applications much easier. + +### restifyError + +This event is emitted following all error events as a generic catch all. It is +recommended to use specific error events to handle specific errors, but this +event can be useful for metrics or logging. If you use this in conjunction with +other error events, the most specific event will be fired first, followed by +this one: + +```js +server.get('/', function(req, res, next) { + return next(new InternalServerError('boom')); +}); + +server.on('InternalServer', function(req, res, err, callback) { + // this will get fired first, as it's the most relevant listener + return callback(); +}); + +server.on('restifyError', function(req, res, err, callback) { + // this is fired second. + return callback(); +}); +``` + +### after + +After each request has been fully serviced, an `after` event is fired. This +event can be hooked into to handle audit logs and other metrics. Note that +flushing a response does not necessarily correspond with an `after` event. +restify considers a request to be fully serviced when either: + +1) The handler chain for a route has been fully completed +2) An error was returned to `next()`, and the corresponding error events have + been fired for that error type + +The signature is for the after event is as follows: + +```js +function(req, res, route, error) { } +``` + +- `req` - the request object +- `res` - the response object +- `route` - the route object that serviced the request +- `error` - the error passed to `next()`, if applicable + +Note that when the server automatically responds with a +NotFound/MethodNotAllowed/VersionNotAllowed, this event will still be fired. + +### pre + +Before each request has been routed, a `pre` event is fired. This event can be +hooked into handle audit logs and other metrics. Since this event fires +_before_ routing has occured, it will fire regardless of whether the route is +supported or not, e.g. requests that result in a `404`. + +The signature for the `pre` event is as follows: + +```js +function(req, res) {} +``` + +- `req` - the request object +- `res` - the response object + +Note that when the server automatically responds with a +NotFound/MethodNotAllowed/VersionNotAllowed, this event will still be fired. + +### routed + +A `routed` event is fired after a request has been routed by the router, but +before handlers specific to that route has run. + +The signature for the `routed` event is as follows: + +```js +function(req, res, route) {} +``` + +- `req` - the request object +- `res` - the response object +- `route` - the route object that serviced the request + +Note that this event will _not_ fire if a requests comes in that are not +routable, i.e. one that would result in a `404`. + +### uncaughtException + +If the restify server was created with `handleUncaughtExceptions: true`, +restify will leverage [domains](https://nodejs.org/api/domain.html) to handle +thrown errors in the handler chain. Thrown errors are a result of an explicit +`throw` statement, or as a result of programmer errors like a typo or a null +ref. These thrown errors are caught by the domain, and will be emitted via this +event. For example: + +```js +server.get('/', function(req, res, next) { + res.send(x); // this will cause a ReferenceError + return next(); +}); + +server.on('uncaughtException', function(req, res, route, err) { + // this event will be fired, with the error object from above: + // ReferenceError: x is not defined +}); +``` + +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 +that was thrown. + +The signature is for the after event is as follows: + +```js +function(req, res, route, error) { } +``` + +- `req` - the request object +- `res` - the response object +- `route` - the route object that serviced the request +- `error` - the error passed to `next()`, if applicable + +### close + +Emitted when the server closes. + + ## Log If you are using the [RequestLogger](#bundled-plugins) plugin, the child logger diff --git a/docs/api/request-events.md b/docs/api/request-events.md new file mode 100644 index 000000000..eb9aa859e --- /dev/null +++ b/docs/api/request-events.md @@ -0,0 +1,21 @@ +### restifyDone + +After request has been fully serviced, an `restifyDone` event is fired. +restify considers a request to be fully serviced when either: + +1) The handler chain for a route has been fully completed +2) An error was returned to `next()`, and the corresponding error events have + been fired for that error type + +The signature for the `restifyDone` event is as follows: + +```js +function(route, error) { } +``` + +* `route` - the route object that serviced the request +* `error` - the error passed to `next()`, if applicable + +Note that when the server automatically responds with a +`NotFound`/`MethodNotAllowed`/`VersionNotAllowed`, this event will still be +fired. diff --git a/docs/config/request.yaml b/docs/config/request.yaml index cfa3e71e2..6c946e5ed 100644 --- a/docs/config/request.yaml +++ b/docs/config/request.yaml @@ -1,4 +1,6 @@ toc: - Request + - name: Events + file: ../api/server-events.md - name: Log file: ../api/request-log.md diff --git a/lib/server.js b/lib/server.js index 4cec6130e..4065e18d1 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1256,7 +1256,9 @@ Server.prototype._finishReqResCycle = function _finishReqResCycle( req._timeFinished = process.hrtime(); // after event has signature of function(req, res, route, err) {...} - self.emit('after', req, res, route, err || res.err); + var finalErr = err || res.err; + req.emit('restifyDone', route, finalErr); + self.emit('after', req, res, route, finalErr); } else { // Store error for when the response is flushed and we actually emit the // 'after' event. The "err" object passed to this method takes diff --git a/test/request.test.js b/test/request.test.js index e283e9bd5..aa40747cc 100644 --- a/test/request.test.js +++ b/test/request.test.js @@ -223,3 +223,55 @@ test('should provide date when request started', function(t) { t.end(); }); }); + +// restifyDone is emitted at the same time when server's after event is emitted, +// you can find more comprehensive testing for `after` lives in server tests. +test('should emit restifyDone event when request is fully served', function(t) { + var clientDone = false; + + SERVER.get('/', function(req, res, next) { + req.on('restifyDone', function(route, err) { + t.ifError(err); + t.ok(route); + setImmediate(function() { + t.ok(clientDone); + t.end(); + }); + }); + + res.send('hello'); + return next(); + }); + + CLIENT.get('/', function(err, _, res) { + t.ifError(err); + t.equal(res.statusCode, 200); + clientDone = true; + }); +}); + +// eslint-disable-next-line max-len +test('should emit restifyDone event when request is fully served with error', function(t) { + var clientDone = false; + + SERVER.get('/', function(req, res, next) { + var myErr = new Error('My Error'); + + req.on('restifyDone', function(route, err) { + t.ok(route); + t.deepEqual(err, myErr); + setImmediate(function() { + t.ok(clientDone); + t.end(); + }); + }); + + return next(myErr); + }); + + CLIENT.get('/', function(err, _, res) { + t.ok(err); + t.equal(res.statusCode, 500); + clientDone = true; + }); +});