From 8a3d170fef881ec1730739ed1e731f106e705ee5 Mon Sep 17 00:00:00 2001 From: retrohacker Date: Fri, 8 Mar 2019 12:31:32 -0800 Subject: [PATCH 01/17] feat(earliest): Handlers that execute before restify --- lib/server.js | 55 +++++++++++++++++++++++++++++++++++++++++++++ test/server.test.js | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/lib/server.js b/lib/server.js index c89dc790a..09392c34e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -117,6 +117,7 @@ function Server(options) { this.onceNext = !!options.onceNext; this.strictNext = !!options.strictNext; + this.earliestChain = []; this.preChain = new Chain({ onceNext: this.onceNext, strictNext: this.strictNext @@ -489,6 +490,51 @@ Server.prototype.pre = function pre() { return this; }; +// eslint-disable-next-line jsdoc/check-param-names +/** + * Gives you hooks that run before restify touches a request. These hooks + * allow you to do processing early in the request/response life cycle without + * the overhead of the restify framework. You can not yield the event loop in + * this handler. + * + * This is useful in cases where you want to make quick decisions about a + * request, for example when shedding load. + * + * The function handler accepts two parameters: req, res. If you want restify + * to ignore this request, return false from your handler. Any other return + * value results in restify handling the request. + * + * @public + * @memberof Server + * @instance + * @function earliest + * @param {...Function} handler - Allows you to add handlers that + * run for all requests, *before* restify touches the request. + * This gives you a hook to change request headers and the like if you need to. + * Note that `req.params` will be undefined, as that's filled in *after* + * routing. + * Takes a function, or an array of functions. + * variable number of nested arrays of handler functions + * @returns {Object} returns self + * @example + * sever.earliest(function(req, res) { + * if(server.inflightRequests > 100) { + * res.statusCode = 503; + * res.end(); + * return false + * } + * return true; + * }) + */ +Server.prototype.earliest = function earliest() { + var args = Array.prototype.slice.call(arguments); + for (var i = 0; i < args.length; i++) { + assert.func(args[i]); + this.earliestChain.push(args[i]); + } + return this; +}; + // eslint-disable-next-line jsdoc/check-param-names /** * Allows you to add in handlers that run for all routes. Note that handlers @@ -807,6 +853,15 @@ Server.prototype.toString = function toString() { Server.prototype._onRequest = function _onRequest(req, res) { var self = this; + // Give the earliest chain the earliest possible opportunity to process + // this request before we do any work on it + var earliestChain = self.earliestChain; + for (var i = 0; i < earliestChain.length; i++) { + if (earliestChain[i](req, res) === false) { + return; + } + } + this.emit('request', req, res); // Skip Socket.io endpoints diff --git a/test/server.test.js b/test/server.test.js index 970b0a6db..1ff321130 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -2683,3 +2683,57 @@ test('should have proxy event handlers as instance', function(t) { t.end(); }); }); + +test('earliest chain should get to reject requests', function(t) { + SERVER.get('/foobar', function(req, res, next) { + t.fail('should not call handler'); + }); + + SERVER.earliest(function(req, res) { + res.statusCode = 413; // I'm a teapot! + res.end(); + return false; + }); + + CLIENT.get('/foobar', function(_, __, res) { + t.equal(res.statusCode, 413); + t.end(); + }); +}); + +test('earliest chain should get to allow requests', function(t) { + SERVER.get('/foobar', function(req, res, next) { + res.send(413, 'Im a teapot'); + return next(); + }); + + SERVER.earliest(function(req, res) { + return true; + }); + + CLIENT.get('/foobar', function(_, __, res) { + t.equal(res.statusCode, 413); + t.end(); + }); +}); + +test('earliest chain should allow multiple handlers', function(t) { + SERVER.get('/foobar', function(req, res, next) { + res.send(413, 'Im a teapot'); + return next(); + }); + + var count = 0; + var handler = function() { + return count++; + }; + + SERVER.earliest(handler, handler, handler); + SERVER.earliest(handler, handler, handler); + + CLIENT.get('/foobar', function(_, __, res) { + t.equal(res.statusCode, 413); + t.equal(count, 6, 'invoked 6 handlers'); + t.end(); + }); +}); From 2e760ddd9cd431d0e8286cdd13144532908d0100 Mon Sep 17 00:00:00 2001 From: retrohacker Date: Fri, 8 Mar 2019 12:54:52 -0800 Subject: [PATCH 02/17] twitch --- lib/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server.js b/lib/server.js index 09392c34e..e9494a47b 100644 --- a/lib/server.js +++ b/lib/server.js @@ -518,7 +518,7 @@ Server.prototype.pre = function pre() { * @returns {Object} returns self * @example * sever.earliest(function(req, res) { - * if(server.inflightRequests > 100) { + * if(server._inflightRequests > 100) { * res.statusCode = 503; * res.end(); * return false From 6f0ecc135f0e927253a2da1df60e96348730cbe2 Mon Sep 17 00:00:00 2001 From: retrohacker Date: Tue, 12 Mar 2019 17:05:43 -0700 Subject: [PATCH 03/17] Revert "twitch" This reverts commit 04ed069bee3fe341abc96d91b9e46b9be44cbd00. --- lib/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server.js b/lib/server.js index e9494a47b..09392c34e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -518,7 +518,7 @@ Server.prototype.pre = function pre() { * @returns {Object} returns self * @example * sever.earliest(function(req, res) { - * if(server._inflightRequests > 100) { + * if(server.inflightRequests > 100) { * res.statusCode = 503; * res.end(); * return false From 3b8d17444af82cbd3c14e1531e4dc9dbdb1a72e7 Mon Sep 17 00:00:00 2001 From: retrohacker Date: Tue, 12 Mar 2019 17:05:55 -0700 Subject: [PATCH 04/17] Revert "feat(earliest): Handlers that execute before restify" This reverts commit 9f4969c9a103c69462f3d6a0c545017bb87bfcda. --- lib/server.js | 55 --------------------------------------------- test/server.test.js | 54 -------------------------------------------- 2 files changed, 109 deletions(-) diff --git a/lib/server.js b/lib/server.js index 09392c34e..c89dc790a 100644 --- a/lib/server.js +++ b/lib/server.js @@ -117,7 +117,6 @@ function Server(options) { this.onceNext = !!options.onceNext; this.strictNext = !!options.strictNext; - this.earliestChain = []; this.preChain = new Chain({ onceNext: this.onceNext, strictNext: this.strictNext @@ -490,51 +489,6 @@ Server.prototype.pre = function pre() { return this; }; -// eslint-disable-next-line jsdoc/check-param-names -/** - * Gives you hooks that run before restify touches a request. These hooks - * allow you to do processing early in the request/response life cycle without - * the overhead of the restify framework. You can not yield the event loop in - * this handler. - * - * This is useful in cases where you want to make quick decisions about a - * request, for example when shedding load. - * - * The function handler accepts two parameters: req, res. If you want restify - * to ignore this request, return false from your handler. Any other return - * value results in restify handling the request. - * - * @public - * @memberof Server - * @instance - * @function earliest - * @param {...Function} handler - Allows you to add handlers that - * run for all requests, *before* restify touches the request. - * This gives you a hook to change request headers and the like if you need to. - * Note that `req.params` will be undefined, as that's filled in *after* - * routing. - * Takes a function, or an array of functions. - * variable number of nested arrays of handler functions - * @returns {Object} returns self - * @example - * sever.earliest(function(req, res) { - * if(server.inflightRequests > 100) { - * res.statusCode = 503; - * res.end(); - * return false - * } - * return true; - * }) - */ -Server.prototype.earliest = function earliest() { - var args = Array.prototype.slice.call(arguments); - for (var i = 0; i < args.length; i++) { - assert.func(args[i]); - this.earliestChain.push(args[i]); - } - return this; -}; - // eslint-disable-next-line jsdoc/check-param-names /** * Allows you to add in handlers that run for all routes. Note that handlers @@ -853,15 +807,6 @@ Server.prototype.toString = function toString() { Server.prototype._onRequest = function _onRequest(req, res) { var self = this; - // Give the earliest chain the earliest possible opportunity to process - // this request before we do any work on it - var earliestChain = self.earliestChain; - for (var i = 0; i < earliestChain.length; i++) { - if (earliestChain[i](req, res) === false) { - return; - } - } - this.emit('request', req, res); // Skip Socket.io endpoints diff --git a/test/server.test.js b/test/server.test.js index 1ff321130..970b0a6db 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -2683,57 +2683,3 @@ test('should have proxy event handlers as instance', function(t) { t.end(); }); }); - -test('earliest chain should get to reject requests', function(t) { - SERVER.get('/foobar', function(req, res, next) { - t.fail('should not call handler'); - }); - - SERVER.earliest(function(req, res) { - res.statusCode = 413; // I'm a teapot! - res.end(); - return false; - }); - - CLIENT.get('/foobar', function(_, __, res) { - t.equal(res.statusCode, 413); - t.end(); - }); -}); - -test('earliest chain should get to allow requests', function(t) { - SERVER.get('/foobar', function(req, res, next) { - res.send(413, 'Im a teapot'); - return next(); - }); - - SERVER.earliest(function(req, res) { - return true; - }); - - CLIENT.get('/foobar', function(_, __, res) { - t.equal(res.statusCode, 413); - t.end(); - }); -}); - -test('earliest chain should allow multiple handlers', function(t) { - SERVER.get('/foobar', function(req, res, next) { - res.send(413, 'Im a teapot'); - return next(); - }); - - var count = 0; - var handler = function() { - return count++; - }; - - SERVER.earliest(handler, handler, handler); - SERVER.earliest(handler, handler, handler); - - CLIENT.get('/foobar', function(_, __, res) { - t.equal(res.statusCode, 413); - t.equal(count, 6, 'invoked 6 handlers'); - t.end(); - }); -}); From 768b3ff733e1e14e6f9c228bfc9aab4fca0884dc Mon Sep 17 00:00:00 2001 From: retrohacker Date: Tue, 12 Mar 2019 17:21:37 -0700 Subject: [PATCH 05/17] Migrate implementation to match express .handle --- lib/server.js | 2 ++ test/server.test.js | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/server.js b/lib/server.js index c89dc790a..cc139dfd9 100644 --- a/lib/server.js +++ b/lib/server.js @@ -838,6 +838,8 @@ Server.prototype._onRequest = function _onRequest(req, res) { } }; +Server.prototype.handle = Server.prototype._onRequest; + /** * Run pre handlers * diff --git a/test/server.test.js b/test/server.test.js index 970b0a6db..0ec342d0e 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -2683,3 +2683,24 @@ test('should have proxy event handlers as instance', function(t) { t.end(); }); }); + +test('handle should invoke restify', function(t) { + var RESTIFY_SERVER = SERVER; + SERVER = http.createServer(); + SERVER.on('request', function(req, res) { + return RESTIFY_SERVER.handle(req, res); + }); + RESTIFY_SERVER.get('/', function(req, res, next) { + res.send(413); + next(); + }); + RESTIFY_SERVER.close(function() { + SERVER.listen(PORT, function() { + CLIENT.get('/', function(err, _, res) { + t.ok(err); + t.equal(res.statusCode, 413); + t.done(); + }); + }); + }); +}); From 1499b4e8e289be743b1e9c13388cbbbd8adc52cd Mon Sep 17 00:00:00 2001 From: retrohacker Date: Wed, 13 Mar 2019 12:05:09 -0700 Subject: [PATCH 06/17] Revert "Migrate implementation to match express .handle" This reverts commit d57ef12f3a93c06f8b876aeeeeec5a0fbe5dc86b. --- lib/server.js | 2 -- test/server.test.js | 21 --------------------- 2 files changed, 23 deletions(-) diff --git a/lib/server.js b/lib/server.js index cc139dfd9..c89dc790a 100644 --- a/lib/server.js +++ b/lib/server.js @@ -838,8 +838,6 @@ Server.prototype._onRequest = function _onRequest(req, res) { } }; -Server.prototype.handle = Server.prototype._onRequest; - /** * Run pre handlers * diff --git a/test/server.test.js b/test/server.test.js index 0ec342d0e..970b0a6db 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -2683,24 +2683,3 @@ test('should have proxy event handlers as instance', function(t) { t.end(); }); }); - -test('handle should invoke restify', function(t) { - var RESTIFY_SERVER = SERVER; - SERVER = http.createServer(); - SERVER.on('request', function(req, res) { - return RESTIFY_SERVER.handle(req, res); - }); - RESTIFY_SERVER.get('/', function(req, res, next) { - res.send(413); - next(); - }); - RESTIFY_SERVER.close(function() { - SERVER.listen(PORT, function() { - CLIENT.get('/', function(err, _, res) { - t.ok(err); - t.equal(res.statusCode, 413); - t.done(); - }); - }); - }); -}); From 36d63fd38f16037dc956cfbe08e37e686a3227b0 Mon Sep 17 00:00:00 2001 From: retrohacker Date: Wed, 13 Mar 2019 12:05:18 -0700 Subject: [PATCH 07/17] Revert "Revert "feat(earliest): Handlers that execute before restify"" This reverts commit d4cbe838f091b51d93cb9b7a7032261d59a92c88. --- lib/server.js | 55 +++++++++++++++++++++++++++++++++++++++++++++ test/server.test.js | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/lib/server.js b/lib/server.js index c89dc790a..09392c34e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -117,6 +117,7 @@ function Server(options) { this.onceNext = !!options.onceNext; this.strictNext = !!options.strictNext; + this.earliestChain = []; this.preChain = new Chain({ onceNext: this.onceNext, strictNext: this.strictNext @@ -489,6 +490,51 @@ Server.prototype.pre = function pre() { return this; }; +// eslint-disable-next-line jsdoc/check-param-names +/** + * Gives you hooks that run before restify touches a request. These hooks + * allow you to do processing early in the request/response life cycle without + * the overhead of the restify framework. You can not yield the event loop in + * this handler. + * + * This is useful in cases where you want to make quick decisions about a + * request, for example when shedding load. + * + * The function handler accepts two parameters: req, res. If you want restify + * to ignore this request, return false from your handler. Any other return + * value results in restify handling the request. + * + * @public + * @memberof Server + * @instance + * @function earliest + * @param {...Function} handler - Allows you to add handlers that + * run for all requests, *before* restify touches the request. + * This gives you a hook to change request headers and the like if you need to. + * Note that `req.params` will be undefined, as that's filled in *after* + * routing. + * Takes a function, or an array of functions. + * variable number of nested arrays of handler functions + * @returns {Object} returns self + * @example + * sever.earliest(function(req, res) { + * if(server.inflightRequests > 100) { + * res.statusCode = 503; + * res.end(); + * return false + * } + * return true; + * }) + */ +Server.prototype.earliest = function earliest() { + var args = Array.prototype.slice.call(arguments); + for (var i = 0; i < args.length; i++) { + assert.func(args[i]); + this.earliestChain.push(args[i]); + } + return this; +}; + // eslint-disable-next-line jsdoc/check-param-names /** * Allows you to add in handlers that run for all routes. Note that handlers @@ -807,6 +853,15 @@ Server.prototype.toString = function toString() { Server.prototype._onRequest = function _onRequest(req, res) { var self = this; + // Give the earliest chain the earliest possible opportunity to process + // this request before we do any work on it + var earliestChain = self.earliestChain; + for (var i = 0; i < earliestChain.length; i++) { + if (earliestChain[i](req, res) === false) { + return; + } + } + this.emit('request', req, res); // Skip Socket.io endpoints diff --git a/test/server.test.js b/test/server.test.js index 970b0a6db..1ff321130 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -2683,3 +2683,57 @@ test('should have proxy event handlers as instance', function(t) { t.end(); }); }); + +test('earliest chain should get to reject requests', function(t) { + SERVER.get('/foobar', function(req, res, next) { + t.fail('should not call handler'); + }); + + SERVER.earliest(function(req, res) { + res.statusCode = 413; // I'm a teapot! + res.end(); + return false; + }); + + CLIENT.get('/foobar', function(_, __, res) { + t.equal(res.statusCode, 413); + t.end(); + }); +}); + +test('earliest chain should get to allow requests', function(t) { + SERVER.get('/foobar', function(req, res, next) { + res.send(413, 'Im a teapot'); + return next(); + }); + + SERVER.earliest(function(req, res) { + return true; + }); + + CLIENT.get('/foobar', function(_, __, res) { + t.equal(res.statusCode, 413); + t.end(); + }); +}); + +test('earliest chain should allow multiple handlers', function(t) { + SERVER.get('/foobar', function(req, res, next) { + res.send(413, 'Im a teapot'); + return next(); + }); + + var count = 0; + var handler = function() { + return count++; + }; + + SERVER.earliest(handler, handler, handler); + SERVER.earliest(handler, handler, handler); + + CLIENT.get('/foobar', function(_, __, res) { + t.equal(res.statusCode, 413); + t.equal(count, 6, 'invoked 6 handlers'); + t.end(); + }); +}); From 0ad283077e294c0ec7610a5706ecb38f78faa5af Mon Sep 17 00:00:00 2001 From: retrohacker Date: Wed, 13 Mar 2019 12:05:26 -0700 Subject: [PATCH 08/17] Revert "Revert "twitch"" This reverts commit c2b855313cf4858f92c3189c446414c1c927bbad. --- lib/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server.js b/lib/server.js index 09392c34e..e9494a47b 100644 --- a/lib/server.js +++ b/lib/server.js @@ -518,7 +518,7 @@ Server.prototype.pre = function pre() { * @returns {Object} returns self * @example * sever.earliest(function(req, res) { - * if(server.inflightRequests > 100) { + * if(server._inflightRequests > 100) { * res.statusCode = 503; * res.end(); * return false From 46f396ddff2e248b49c4e9c8d1d2f9062574811d Mon Sep 17 00:00:00 2001 From: retrohacker Date: Wed, 13 Mar 2019 17:44:26 -0700 Subject: [PATCH 09/17] use chain --- lib/server.js | 63 +++++++++++++++++++++++++++++++-------------- test/server.test.js | 13 +++++----- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/lib/server.js b/lib/server.js index e9494a47b..1d8dd18e6 100644 --- a/lib/server.js +++ b/lib/server.js @@ -117,7 +117,10 @@ function Server(options) { this.onceNext = !!options.onceNext; this.strictNext = !!options.strictNext; - this.earliestChain = []; + this.earliestChain = new Chain({ + onceNext: this.onceNext, + strictNext: this.strictNext + }); this.preChain = new Chain({ onceNext: this.onceNext, strictNext: this.strictNext @@ -218,18 +221,36 @@ function Server(options) { res.writeContinue(); } - self._onRequest(req, res); + self.earliestChain.run(req, res, function earliestDone(ignore) { + if (ignore === false || Boolean(ignore)) { + return; + } + self._onRequest(req, res); + }); }); if (options.handleUpgrades) { this.server.on('upgrade', function onUpgrade(req, socket, head) { - req._upgradeRequest = true; var res = upgrade.createResponse(req, socket, head); - self._onRequest(req, res); + req._upgradeRequest = true; + self.earliestChain.run(req, res, function earliestDone(ignore) { + if (ignore === false || Boolean(ignore)) { + return; + } + self._onRequest(req, res); + }); }); } - this.server.on('request', this._onRequest.bind(this)); + this.server.on('request', function onRequest(req, res) { + self.earliestChain.run(req, res, function earliestDone(ignore) { + if (ignore === false || Boolean(ignore)) { + return; + } + + self._onRequest(req, res); + }); + }); this.__defineGetter__('maxHeadersCount', function getMaxHeadersCount() { return self.server.maxHeadersCount; @@ -527,11 +548,15 @@ Server.prototype.pre = function pre() { * }) */ Server.prototype.earliest = function earliest() { - var args = Array.prototype.slice.call(arguments); - for (var i = 0; i < args.length; i++) { - assert.func(args[i]); - this.earliestChain.push(args[i]); - } + var self = this; + var handlers = Array.prototype.slice.call(arguments); + + argumentsToChain(handlers).forEach(function forEach(handler) { + handler._name = + handler.name || 'earliest-' + self.earliestChain.count(); + self.earliestChain.add(handler); + }); + return this; }; @@ -710,6 +735,9 @@ Server.prototype.getDebugInfo = function getDebugInfo() { // output the actual routes registered with restify var routeInfo = self.router.getDebugInfo(); + var earliestHandlers = self.earliestChain + .getHandlers() + .map(funcNameMapper); var preHandlers = self.preChain.getHandlers().map(funcNameMapper); var useHandlers = self.useChain.getHandlers().map(funcNameMapper); @@ -731,6 +759,7 @@ Server.prototype.getDebugInfo = function getDebugInfo() { address: addressInfo && addressInfo.address, port: addressInfo && addressInfo.port, inflightRequests: self.inflightRequests(), + earliest: earliestHandlers, pre: preHandlers, use: useHandlers, after: self.listeners('after').map(funcNameMapper) @@ -792,6 +821,11 @@ Server.prototype.toString = function toString() { str += sprintf(LINE_FMT, 'Accepts', this.acceptable.join(', ')); str += sprintf(LINE_FMT, 'Name', this.name); + str += sprintf( + LINE_FMT, + 'Earliest', + handlersToString(this.earliestChain.getHandlers()) + ); str += sprintf( LINE_FMT, 'Pre', @@ -853,15 +887,6 @@ Server.prototype.toString = function toString() { Server.prototype._onRequest = function _onRequest(req, res) { var self = this; - // Give the earliest chain the earliest possible opportunity to process - // this request before we do any work on it - var earliestChain = self.earliestChain; - for (var i = 0; i < earliestChain.length; i++) { - if (earliestChain[i](req, res) === false) { - return; - } - } - this.emit('request', req, res); // Skip Socket.io endpoints diff --git a/test/server.test.js b/test/server.test.js index 1ff321130..7872f1080 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -2689,10 +2689,10 @@ test('earliest chain should get to reject requests', function(t) { t.fail('should not call handler'); }); - SERVER.earliest(function(req, res) { + SERVER.earliest(function(req, res, next) { res.statusCode = 413; // I'm a teapot! res.end(); - return false; + return next(false); }); CLIENT.get('/foobar', function(_, __, res) { @@ -2707,8 +2707,8 @@ test('earliest chain should get to allow requests', function(t) { return next(); }); - SERVER.earliest(function(req, res) { - return true; + SERVER.earliest(function(req, res, next) { + return next(); }); CLIENT.get('/foobar', function(_, __, res) { @@ -2724,8 +2724,9 @@ test('earliest chain should allow multiple handlers', function(t) { }); var count = 0; - var handler = function() { - return count++; + var handler = function(_, __, next) { + count++; + return next(); }; SERVER.earliest(handler, handler, handler); From 88e1d230433070b643c0a480e506ef9032896771 Mon Sep 17 00:00:00 2001 From: retrohacker Date: Wed, 13 Mar 2019 18:42:21 -0700 Subject: [PATCH 10/17] syncronous inflightRequest calc --- lib/server.js | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/lib/server.js b/lib/server.js index 1d8dd18e6..52da2dc9a 100644 --- a/lib/server.js +++ b/lib/server.js @@ -217,12 +217,14 @@ function Server(options) { return; } + self._inflightRequests++; if (!options.noWriteContinue) { res.writeContinue(); } self.earliestChain.run(req, res, function earliestDone(ignore) { if (ignore === false || Boolean(ignore)) { + self._inflightRequests--; return; } self._onRequest(req, res); @@ -231,10 +233,12 @@ function Server(options) { if (options.handleUpgrades) { this.server.on('upgrade', function onUpgrade(req, socket, head) { + self._inflightRequests++; var res = upgrade.createResponse(req, socket, head); req._upgradeRequest = true; self.earliestChain.run(req, res, function earliestDone(ignore) { if (ignore === false || Boolean(ignore)) { + self._inflightRequests--; return; } self._onRequest(req, res); @@ -243,8 +247,10 @@ function Server(options) { } this.server.on('request', function onRequest(req, res) { + self._inflightRequests++; self.earliestChain.run(req, res, function earliestDone(ignore) { if (ignore === false || Boolean(ignore)) { + self._inflightRequests--; return; } @@ -513,17 +519,9 @@ Server.prototype.pre = function pre() { // eslint-disable-next-line jsdoc/check-param-names /** - * Gives you hooks that run before restify touches a request. These hooks - * allow you to do processing early in the request/response life cycle without - * the overhead of the restify framework. You can not yield the event loop in - * this handler. - * - * This is useful in cases where you want to make quick decisions about a - * request, for example when shedding load. - * - * The function handler accepts two parameters: req, res. If you want restify - * to ignore this request, return false from your handler. Any other return - * value results in restify handling the request. + * Gives you hooks to run _before_ restify touches the request. This gives you + * a chance to do processing early in the request/response lifecycle without the + * overhead of the restify framework. * * @public * @memberof Server @@ -534,17 +532,17 @@ Server.prototype.pre = function pre() { * This gives you a hook to change request headers and the like if you need to. * Note that `req.params` will be undefined, as that's filled in *after* * routing. - * Takes a function, or an array of functions. - * variable number of nested arrays of handler functions + * Takes a function, or a ariable number of nested arrays of handler functions * @returns {Object} returns self * @example - * sever.earliest(function(req, res) { + * var error_response = new Error(); + * sever.earliest(function(req, res, next) { * if(server._inflightRequests > 100) { * res.statusCode = 503; * res.end(); - * return false + * next(error_response); * } - * return true; + * return next(); * }) */ Server.prototype.earliest = function earliest() { @@ -897,9 +895,6 @@ Server.prototype._onRequest = function _onRequest(req, res) { // Decorate req and res objects self._setupRequest(req, res); - // increment number of requests - self._inflightRequests++; - // Run in domain to catch async errors // It has significant negative performance impact // Warning: this feature depends on the deprecated domains module From 4129031bcc7a8db2f98895207cda798a1bf0cb7b Mon Sep 17 00:00:00 2001 From: retrohacker Date: Thu, 14 Mar 2019 11:02:27 -0700 Subject: [PATCH 11/17] Revert "syncronous inflightRequest calc" This reverts commit 5b26d642b5ed67a8c65acf36cded79be2b423911. --- lib/server.js | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/server.js b/lib/server.js index 52da2dc9a..1d8dd18e6 100644 --- a/lib/server.js +++ b/lib/server.js @@ -217,14 +217,12 @@ function Server(options) { return; } - self._inflightRequests++; if (!options.noWriteContinue) { res.writeContinue(); } self.earliestChain.run(req, res, function earliestDone(ignore) { if (ignore === false || Boolean(ignore)) { - self._inflightRequests--; return; } self._onRequest(req, res); @@ -233,12 +231,10 @@ function Server(options) { if (options.handleUpgrades) { this.server.on('upgrade', function onUpgrade(req, socket, head) { - self._inflightRequests++; var res = upgrade.createResponse(req, socket, head); req._upgradeRequest = true; self.earliestChain.run(req, res, function earliestDone(ignore) { if (ignore === false || Boolean(ignore)) { - self._inflightRequests--; return; } self._onRequest(req, res); @@ -247,10 +243,8 @@ function Server(options) { } this.server.on('request', function onRequest(req, res) { - self._inflightRequests++; self.earliestChain.run(req, res, function earliestDone(ignore) { if (ignore === false || Boolean(ignore)) { - self._inflightRequests--; return; } @@ -519,9 +513,17 @@ Server.prototype.pre = function pre() { // eslint-disable-next-line jsdoc/check-param-names /** - * Gives you hooks to run _before_ restify touches the request. This gives you - * a chance to do processing early in the request/response lifecycle without the - * overhead of the restify framework. + * Gives you hooks that run before restify touches a request. These hooks + * allow you to do processing early in the request/response life cycle without + * the overhead of the restify framework. You can not yield the event loop in + * this handler. + * + * This is useful in cases where you want to make quick decisions about a + * request, for example when shedding load. + * + * The function handler accepts two parameters: req, res. If you want restify + * to ignore this request, return false from your handler. Any other return + * value results in restify handling the request. * * @public * @memberof Server @@ -532,17 +534,17 @@ Server.prototype.pre = function pre() { * This gives you a hook to change request headers and the like if you need to. * Note that `req.params` will be undefined, as that's filled in *after* * routing. - * Takes a function, or a ariable number of nested arrays of handler functions + * Takes a function, or an array of functions. + * variable number of nested arrays of handler functions * @returns {Object} returns self * @example - * var error_response = new Error(); - * sever.earliest(function(req, res, next) { + * sever.earliest(function(req, res) { * if(server._inflightRequests > 100) { * res.statusCode = 503; * res.end(); - * next(error_response); + * return false * } - * return next(); + * return true; * }) */ Server.prototype.earliest = function earliest() { @@ -895,6 +897,9 @@ Server.prototype._onRequest = function _onRequest(req, res) { // Decorate req and res objects self._setupRequest(req, res); + // increment number of requests + self._inflightRequests++; + // Run in domain to catch async errors // It has significant negative performance impact // Warning: this feature depends on the deprecated domains module From d9fb26d1d5782f509a3744d8b752cc32e8629a29 Mon Sep 17 00:00:00 2001 From: retrohacker Date: Thu, 14 Mar 2019 11:02:41 -0700 Subject: [PATCH 12/17] Revert "use chain" This reverts commit 7db83a50580a7932b4def240deac4fbee29f1675. --- lib/server.js | 63 ++++++++++++++------------------------------- test/server.test.js | 13 +++++----- 2 files changed, 25 insertions(+), 51 deletions(-) diff --git a/lib/server.js b/lib/server.js index 1d8dd18e6..e9494a47b 100644 --- a/lib/server.js +++ b/lib/server.js @@ -117,10 +117,7 @@ function Server(options) { this.onceNext = !!options.onceNext; this.strictNext = !!options.strictNext; - this.earliestChain = new Chain({ - onceNext: this.onceNext, - strictNext: this.strictNext - }); + this.earliestChain = []; this.preChain = new Chain({ onceNext: this.onceNext, strictNext: this.strictNext @@ -221,36 +218,18 @@ function Server(options) { res.writeContinue(); } - self.earliestChain.run(req, res, function earliestDone(ignore) { - if (ignore === false || Boolean(ignore)) { - return; - } - self._onRequest(req, res); - }); + self._onRequest(req, res); }); if (options.handleUpgrades) { this.server.on('upgrade', function onUpgrade(req, socket, head) { - var res = upgrade.createResponse(req, socket, head); req._upgradeRequest = true; - self.earliestChain.run(req, res, function earliestDone(ignore) { - if (ignore === false || Boolean(ignore)) { - return; - } - self._onRequest(req, res); - }); + var res = upgrade.createResponse(req, socket, head); + self._onRequest(req, res); }); } - this.server.on('request', function onRequest(req, res) { - self.earliestChain.run(req, res, function earliestDone(ignore) { - if (ignore === false || Boolean(ignore)) { - return; - } - - self._onRequest(req, res); - }); - }); + this.server.on('request', this._onRequest.bind(this)); this.__defineGetter__('maxHeadersCount', function getMaxHeadersCount() { return self.server.maxHeadersCount; @@ -548,15 +527,11 @@ Server.prototype.pre = function pre() { * }) */ Server.prototype.earliest = function earliest() { - var self = this; - var handlers = Array.prototype.slice.call(arguments); - - argumentsToChain(handlers).forEach(function forEach(handler) { - handler._name = - handler.name || 'earliest-' + self.earliestChain.count(); - self.earliestChain.add(handler); - }); - + var args = Array.prototype.slice.call(arguments); + for (var i = 0; i < args.length; i++) { + assert.func(args[i]); + this.earliestChain.push(args[i]); + } return this; }; @@ -735,9 +710,6 @@ Server.prototype.getDebugInfo = function getDebugInfo() { // output the actual routes registered with restify var routeInfo = self.router.getDebugInfo(); - var earliestHandlers = self.earliestChain - .getHandlers() - .map(funcNameMapper); var preHandlers = self.preChain.getHandlers().map(funcNameMapper); var useHandlers = self.useChain.getHandlers().map(funcNameMapper); @@ -759,7 +731,6 @@ Server.prototype.getDebugInfo = function getDebugInfo() { address: addressInfo && addressInfo.address, port: addressInfo && addressInfo.port, inflightRequests: self.inflightRequests(), - earliest: earliestHandlers, pre: preHandlers, use: useHandlers, after: self.listeners('after').map(funcNameMapper) @@ -821,11 +792,6 @@ Server.prototype.toString = function toString() { str += sprintf(LINE_FMT, 'Accepts', this.acceptable.join(', ')); str += sprintf(LINE_FMT, 'Name', this.name); - str += sprintf( - LINE_FMT, - 'Earliest', - handlersToString(this.earliestChain.getHandlers()) - ); str += sprintf( LINE_FMT, 'Pre', @@ -887,6 +853,15 @@ Server.prototype.toString = function toString() { Server.prototype._onRequest = function _onRequest(req, res) { var self = this; + // Give the earliest chain the earliest possible opportunity to process + // this request before we do any work on it + var earliestChain = self.earliestChain; + for (var i = 0; i < earliestChain.length; i++) { + if (earliestChain[i](req, res) === false) { + return; + } + } + this.emit('request', req, res); // Skip Socket.io endpoints diff --git a/test/server.test.js b/test/server.test.js index 7872f1080..1ff321130 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -2689,10 +2689,10 @@ test('earliest chain should get to reject requests', function(t) { t.fail('should not call handler'); }); - SERVER.earliest(function(req, res, next) { + SERVER.earliest(function(req, res) { res.statusCode = 413; // I'm a teapot! res.end(); - return next(false); + return false; }); CLIENT.get('/foobar', function(_, __, res) { @@ -2707,8 +2707,8 @@ test('earliest chain should get to allow requests', function(t) { return next(); }); - SERVER.earliest(function(req, res, next) { - return next(); + SERVER.earliest(function(req, res) { + return true; }); CLIENT.get('/foobar', function(_, __, res) { @@ -2724,9 +2724,8 @@ test('earliest chain should allow multiple handlers', function(t) { }); var count = 0; - var handler = function(_, __, next) { - count++; - return next(); + var handler = function() { + return count++; }; SERVER.earliest(handler, handler, handler); From 6bfadcc8bc56ebaab9a18af72a3fe628dc035f48 Mon Sep 17 00:00:00 2001 From: retrohacker Date: Thu, 14 Mar 2019 11:06:07 -0700 Subject: [PATCH 13/17] synchronous inflight accounting --- lib/server.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/server.js b/lib/server.js index e9494a47b..a07d7827c 100644 --- a/lib/server.js +++ b/lib/server.js @@ -853,11 +853,15 @@ Server.prototype.toString = function toString() { Server.prototype._onRequest = function _onRequest(req, res) { var self = this; + // increment number of requests + self._inflightRequests++; + // Give the earliest chain the earliest possible opportunity to process // this request before we do any work on it var earliestChain = self.earliestChain; for (var i = 0; i < earliestChain.length; i++) { if (earliestChain[i](req, res) === false) { + self._inflightRequests--; return; } } @@ -866,15 +870,13 @@ Server.prototype._onRequest = function _onRequest(req, res) { // Skip Socket.io endpoints if (this.socketio && /^\/socket\.io.*/.test(req.url)) { + self._inflightRequests--; return; } // Decorate req and res objects self._setupRequest(req, res); - // increment number of requests - self._inflightRequests++; - // Run in domain to catch async errors // It has significant negative performance impact // Warning: this feature depends on the deprecated domains module From b066c4c63d8004ed764a34875038febc9d8e0259 Mon Sep 17 00:00:00 2001 From: retrohacker Date: Fri, 15 Mar 2019 12:07:21 -0700 Subject: [PATCH 14/17] forward compat by limiting range of accepted return types --- lib/server.js | 13 ++++++++++++- test/server.test.js | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/server.js b/lib/server.js index a07d7827c..c023d3dff 100644 --- a/lib/server.js +++ b/lib/server.js @@ -860,7 +860,18 @@ Server.prototype._onRequest = function _onRequest(req, res) { // this request before we do any work on it var earliestChain = self.earliestChain; for (var i = 0; i < earliestChain.length; i++) { - if (earliestChain[i](req, res) === false) { + var handle = earliestChain[i](req, res); + assert.ok( + handle === true || + handle === false || + handle === undefined || + handle === null, + 'Return value of earliest[' + + i + + '] must be: ' + + 'boolean, undefined, or null' + ); + if (handle === false) { self._inflightRequests--; return; } diff --git a/test/server.test.js b/test/server.test.js index 1ff321130..e88603b20 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -2725,7 +2725,7 @@ test('earliest chain should allow multiple handlers', function(t) { var count = 0; var handler = function() { - return count++; + count++; }; SERVER.earliest(handler, handler, handler); From c5c9ddb9c39e582472a0f98ad046b17102743280 Mon Sep 17 00:00:00 2001 From: retrohacker Date: Fri, 15 Mar 2019 14:23:52 -0700 Subject: [PATCH 15/17] first instead of earliest and better docs --- lib/server.js | 58 ++++++++++++++++++++++++++++++++------------- test/server.test.js | 14 +++++------ 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/lib/server.js b/lib/server.js index c023d3dff..5d175bd0f 100644 --- a/lib/server.js +++ b/lib/server.js @@ -117,7 +117,7 @@ function Server(options) { this.onceNext = !!options.onceNext; this.strictNext = !!options.strictNext; - this.earliestChain = []; + this.firstChain = []; this.preChain = new Chain({ onceNext: this.onceNext, strictNext: this.strictNext @@ -501,23 +501,33 @@ Server.prototype.pre = function pre() { * request, for example when shedding load. * * The function handler accepts two parameters: req, res. If you want restify - * to ignore this request, return false from your handler. Any other return - * value results in restify handling the request. + * to ignore this request, return false from your handler. Return true or + * undefined to let restify continue handling the request. + * + * When false is returned, restify immediately stops handling the request. This + * means that no further middleware will be executed for any chain and routing + * will not occure. All request/response handling for an incoming request must + * be done inside the first handler if you intend to return false. This + * includes things like closing the response and returning a status code. + * + * The only work restify does for a first handler is to increment the number of + * inflightRequests prior to calling the chain, and decrement that value if the + * handler returns false. * * @public * @memberof Server * @instance - * @function earliest + * @function first * @param {...Function} handler - Allows you to add handlers that * run for all requests, *before* restify touches the request. * This gives you a hook to change request headers and the like if you need to. * Note that `req.params` will be undefined, as that's filled in *after* * routing. - * Takes a function, or an array of functions. - * variable number of nested arrays of handler functions + + * Takes one or more functions. * @returns {Object} returns self * @example - * sever.earliest(function(req, res) { + * sever.first(function(req, res) { * if(server._inflightRequests > 100) { * res.statusCode = 503; * res.end(); @@ -526,11 +536,11 @@ Server.prototype.pre = function pre() { * return true; * }) */ -Server.prototype.earliest = function earliest() { +Server.prototype.first = function first() { var args = Array.prototype.slice.call(arguments); for (var i = 0; i < args.length; i++) { assert.func(args[i]); - this.earliestChain.push(args[i]); + this.firstChain.push(args[i]); } return this; }; @@ -853,24 +863,40 @@ Server.prototype.toString = function toString() { Server.prototype._onRequest = function _onRequest(req, res) { var self = this; - // increment number of requests + // Increment the number of inflight requests prior to calling the firstChain + // handlers. This accomplishes two things. First, it gives earliest an + // accurate count of how many inflight requests there would be including + // this new request. Second, it intentionally winds up the inflight request + // accounting with the implementation of firstChain. Note how we increment + // here, but decrement down inside the for loop below. It's easy to end up + // with race conditions betwen inflight request accounting and inflight + // request load shedding, causing load shedding to reject/allow too many + // requests. The current implementation of firstChain is designed to + // remove those race conditions. By winding these implementations up with + // one another, it makes it clear that moving around the inflight request + // accounting will change the behavior of earliest. self._inflightRequests++; - // Give the earliest chain the earliest possible opportunity to process - // this request before we do any work on it - var earliestChain = self.earliestChain; - for (var i = 0; i < earliestChain.length; i++) { - var handle = earliestChain[i](req, res); + // Give the first chain the earliest possible opportunity to process + // this request before we do any work on it. + var firstChain = self.firstChain; + for (var i = 0; i < firstChain.length; i++) { + var handle = firstChain[i](req, res); + // Limit the range of values we will accept as return results of + // first handlers. This helps us maintain forward compatibility by + // ensuring users don't rely on undocumented/unspecified behavior. assert.ok( handle === true || handle === false || handle === undefined || handle === null, - 'Return value of earliest[' + + 'Return value of first[' + i + '] must be: ' + 'boolean, undefined, or null' ); + // If the first handler returns false, stop handling the request + // immediately. if (handle === false) { self._inflightRequests--; return; diff --git a/test/server.test.js b/test/server.test.js index e88603b20..636d12a99 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -2684,12 +2684,12 @@ test('should have proxy event handlers as instance', function(t) { }); }); -test('earliest chain should get to reject requests', function(t) { +test('first chain should get to reject requests', function(t) { SERVER.get('/foobar', function(req, res, next) { t.fail('should not call handler'); }); - SERVER.earliest(function(req, res) { + SERVER.first(function(req, res) { res.statusCode = 413; // I'm a teapot! res.end(); return false; @@ -2701,13 +2701,13 @@ test('earliest chain should get to reject requests', function(t) { }); }); -test('earliest chain should get to allow requests', function(t) { +test('first chain should get to allow requests', function(t) { SERVER.get('/foobar', function(req, res, next) { res.send(413, 'Im a teapot'); return next(); }); - SERVER.earliest(function(req, res) { + SERVER.first(function(req, res) { return true; }); @@ -2717,7 +2717,7 @@ test('earliest chain should get to allow requests', function(t) { }); }); -test('earliest chain should allow multiple handlers', function(t) { +test('first chain should allow multiple handlers', function(t) { SERVER.get('/foobar', function(req, res, next) { res.send(413, 'Im a teapot'); return next(); @@ -2728,8 +2728,8 @@ test('earliest chain should allow multiple handlers', function(t) { count++; }; - SERVER.earliest(handler, handler, handler); - SERVER.earliest(handler, handler, handler); + SERVER.first(handler, handler, handler); + SERVER.first(handler, handler, handler); CLIENT.get('/foobar', function(_, __, res) { t.equal(res.statusCode, 413); From 703701312d5f1aa33e369eae1cf1640cfccffc0b Mon Sep 17 00:00:00 2001 From: retrohacker Date: Fri, 15 Mar 2019 15:58:39 -0700 Subject: [PATCH 16/17] moar tests --- test/server.test.js | 102 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/test/server.test.js b/test/server.test.js index 636d12a99..ba19cbb55 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -2737,3 +2737,105 @@ test('first chain should allow multiple handlers', function(t) { t.end(); }); }); + +test('first chain should allow any handler to reject', function(t) { + SERVER.get('/foobar', function(req, res, next) { + res.send(200, 'Handled'); + return next(); + }); + + var count = 0; + var handler = function() { + count++; + }; + + var handlerAbort = function(req, res) { + count++; + res.statusCode = 413; + res.end(); + return false; + }; + + SERVER.first(handler, handler, handler); + // Should append these handlers and abort the chain on the second + SERVER.first(handler, handlerAbort, handler); + // These should never run + SERVER.first(handler, handlerAbort); + + CLIENT.get('/foobar', function(_, __, res) { + t.equal(res.statusCode, 413); + t.equal(count, 5, 'invoked 5 handlers'); + t.end(); + }); +}); + +test('inflightRequest accounting stable with firstChain', function(t) { + // Make 3 requests, shed the second, and ensure inflightRequest accounting + // for all the requests + var request = 0; + SERVER.first(function(req, res) { + request++; + + if (request === 1) { + t.equal(SERVER._inflightRequests, 1); + return true; + } + if (request === 2) { + t.equal(SERVER._inflightRequests, 2); + res.statusCode = 413; + res.end(); + return false; + } + if (request === 3) { + // Since the second request was shed, and inflightRequest accounting + // should be happening synchronously, this should still be 2 for + // the third request + t.equal(SERVER._inflightRequests, 2); + return true; + } + + t.fail('Too many requests for test'); + return false; + }); + var nexts = []; + SERVER.get('/foobar', function(req, res, next) { + res.send(200, 'success'); + nexts.push(next); + if (nexts.length === 2) { + nexts.forEach(function(finishRequest) { + finishRequest(); + }); + } + }); + + var results = []; + function getDone(_, __, res) { + results.push(res); + if (results.length < 3) { + return; + } + for (var i = 0; i < results.length; i++) { + // The shed request should always be returned first, since it isn't + // handled by SERVER.get + if (i === 0) { + t.equal( + results[i].statusCode, + 413, + 'results[' + i + '] === 413' + ); + } else { + t.equal( + results[i].statusCode, + 200, + 'results[' + i + '] === 200' + ); + } + } + t.end(); + } + + // kick off all 3 at the same time to see if we can trigger a race condition + CLIENT.get('/foobar', getDone); + CLIENT.get('/foobar', getDone); + CLIENT.get('/foobar', getDone); +}); From 29b3aad05da009bc053b241e0f4c0ac28836cd8f Mon Sep 17 00:00:00 2001 From: retrohacker Date: Mon, 18 Mar 2019 13:08:39 -0700 Subject: [PATCH 17/17] implement feedback --- lib/server.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/server.js b/lib/server.js index 5d175bd0f..e70f012b4 100644 --- a/lib/server.js +++ b/lib/server.js @@ -497,9 +497,6 @@ Server.prototype.pre = function pre() { * the overhead of the restify framework. You can not yield the event loop in * this handler. * - * This is useful in cases where you want to make quick decisions about a - * request, for example when shedding load. - * * The function handler accepts two parameters: req, res. If you want restify * to ignore this request, return false from your handler. Return true or * undefined to let restify continue handling the request. @@ -512,7 +509,22 @@ Server.prototype.pre = function pre() { * * The only work restify does for a first handler is to increment the number of * inflightRequests prior to calling the chain, and decrement that value if the - * handler returns false. + * handler returns false. Returning anything other than true, false, undefined, + * or null will cause an exception to be thrown. + * + * Since server.first is designed to bypass the restify framework, there are + * naturally trade-offs you make when using this API: + * * Standard restify lifecycle events such as 'after' are not triggered for + * any request that you return false from a handler for + * * Invoking any of the restify req/res APIs from within a first handler is + * unspecified behavior, as the restify framework hasn't built up state for + * the request yet. + * * There are no request timers available at the time that the first chain + * runs. + * * And more! Beware doing anything with restify in these handlers. They are + * designed to give you similar access to the req/res as you would have if + * you were directly using node.js' http module, they are outside of the + * restify framework! * * @public * @memberof Server @@ -527,8 +539,8 @@ Server.prototype.pre = function pre() { * Takes one or more functions. * @returns {Object} returns self * @example - * sever.first(function(req, res) { - * if(server._inflightRequests > 100) { + * server.first(function(req, res) { + * if(server.inflightRequests() > 100) { * res.statusCode = 503; * res.end(); * return false