126 lines
4.9 KiB
JavaScript
126 lines
4.9 KiB
JavaScript
'use strict';
|
|
|
|
var assert = require('assert-plus');
|
|
var GatewayTimeoutError = require('restify-errors').GatewayTimeoutError;
|
|
|
|
/**
|
|
* Request Expiry can be used to throttle requests that have already exceeded
|
|
* their client timeouts. Requests can be sent with a configurable client
|
|
* timeout header, e.g. 'x-request-expiry-time', which gives in absolute ms
|
|
* since epoch, when this request will be timed out by the client.
|
|
*
|
|
* This plugin will throttle all incoming requests via a 504 where
|
|
* 'x-request-expiry-time' less than Date.now() -- since these incoming requests
|
|
* have already been timed out by the client. This prevents the server from
|
|
* processing unnecessary requests.
|
|
*
|
|
* Request expiry will use headers to tell if the incoming request has expired.
|
|
* There are two options for this plugin:
|
|
* 1. Absolute Time
|
|
* * Time in Milliseconds since Epoch when this request should be
|
|
* considered expired
|
|
* 2. Timeout
|
|
* * The request start time is supplied
|
|
* * A timeout, in milliseconds, is given
|
|
* * The timeout is added to the request start time to arrive at the
|
|
* absolute time in which the request is considered expired
|
|
*
|
|
* #### Using an external storage mechanism for key/bucket mappings.
|
|
*
|
|
* By default, the restify throttling plugin uses an in-memory LRU to store
|
|
* mappings between throttling keys (i.e., IP address) to the actual bucket that
|
|
* key is consuming. If this suits you, you can tune the maximum number of keys
|
|
* to store in memory with `options.maxKeys`; the default is 10000.
|
|
*
|
|
* In some circumstances, you want to offload this into a shared system, such as
|
|
* Redis, if you have a fleet of API servers and you're not getting steady
|
|
* and/or uniform request distribution. To enable this, you can pass in
|
|
* `options.tokensTable`, which is simply any Object that supports `put` and
|
|
* `get` with a `String` key, and an `Object` value.
|
|
*
|
|
* @public
|
|
* @function requestExpiry
|
|
* @param {Object} opts - an options object
|
|
* @param {String} [opts.absoluteHeader] - The header key to be used for
|
|
* the expiry time of each request.
|
|
* @param {String} opts.startHeader - The header key for the start time
|
|
* of the request.
|
|
* @param {String} opts.timeoutHeader - The header key for the time in
|
|
* milliseconds that should ellapse before
|
|
* the request is considered expired.
|
|
* @returns {Function} Handler
|
|
* @example
|
|
* <caption>
|
|
* The only option provided is `header` which is the request header used
|
|
* to specify the client timeout.
|
|
* </caption>
|
|
* server.use(restify.plugins.requestExpiry({
|
|
* header: 'x-request-expiry-time'
|
|
* });
|
|
*/
|
|
function requestExpiry(opts) {
|
|
assert.object(opts, 'opts');
|
|
assert.optionalString(opts.absoluteHeader, 'opts.absoluteHeader');
|
|
|
|
if (!opts.absoluteHeader) {
|
|
assert.string(opts.startHeader, 'opts.startHeader');
|
|
assert.string(opts.timeoutHeader, 'opts.timeoutHeader');
|
|
}
|
|
|
|
var useAbsolute = opts.absoluteHeader !== undefined;
|
|
var absoluteHeaderKey = opts.absoluteHeader;
|
|
var startHeaderKey = opts.startHeader;
|
|
var timeoutHeaderKey = opts.timeoutHeader;
|
|
|
|
return function requestExpirationCheck(req, res, next) {
|
|
/*
|
|
* Add check expiry API to to req if it doesn't already exist. We only
|
|
* add this the first time this handler is run, since this handler
|
|
* could be used in multiple places in the handler chain.
|
|
*/
|
|
if (!req._expiryTime) {
|
|
// if the headers don't exist, then the request will never expire.
|
|
req._expiryTime = Infinity;
|
|
|
|
if (useAbsolute) {
|
|
var expiryTime = parseInt(req.headers[absoluteHeaderKey], 10);
|
|
|
|
if (!isNaN(expiryTime)) {
|
|
req._expiryTime = expiryTime;
|
|
}
|
|
} else {
|
|
// Use the start time header and add the timeout header to it
|
|
// to arrive at the expiration time
|
|
var startTime = parseInt(req.headers[startHeaderKey], 10);
|
|
var timeout = parseInt(req.headers[timeoutHeaderKey], 10);
|
|
|
|
if (!isNaN(startTime) && !isNaN(timeout)) {
|
|
req._expiryTime = startTime + timeout;
|
|
}
|
|
}
|
|
|
|
req.isExpired = function isExpired() {
|
|
return Date.now() > req._expiryTime;
|
|
};
|
|
}
|
|
|
|
if (req.isExpired()) {
|
|
// The request has expired
|
|
return next(
|
|
new GatewayTimeoutError({
|
|
message: 'Request has expired',
|
|
context: {
|
|
expiryTime: req._expiryTime,
|
|
mode: opts.absoluteHeader ? 'absolute' : 'relative'
|
|
}
|
|
})
|
|
);
|
|
} else {
|
|
// Happy case
|
|
return next();
|
|
}
|
|
};
|
|
}
|
|
|
|
module.exports = requestExpiry;
|