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

Eps1lon/add throttle headers #1453

Merged
merged 3 commits into from Aug 17, 2017
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: 13 additions & 1 deletion lib/plugins/throttle.js
Expand Up @@ -204,6 +204,8 @@ TokenTable.prototype.get = function get(key) {
* - {Boolean} ip (optional).
* - {Boolean} username (optional).
* - {Boolean} xff (optional).
* - {Boolean} setHeaders: Set response headers for rate,
* limit (burst) and remaining. Default is false.
* - {Object} overrides (optional).
* - {Object} tokensTable: a storage engine this plugin will
* use to store throttling keys -> bucket mappings.
Expand All @@ -220,6 +222,7 @@ function throttle(options) {
assert.object(options, 'options');
assert.number(options.burst, 'options.burst');
assert.number(options.rate, 'options.rate');
assert.optionalBool(options.setHeaders, 'options.setHeaders');

if (!xor(options.ip, options.xff, options.username)) {
throw new Error('(ip ^ username ^ xff)');
Expand Down Expand Up @@ -278,7 +281,16 @@ function throttle(options) {
req.log.trace('Throttle(%s): num_tokens= %d',
attr, bucket.tokens);

if (!bucket.consume(1)) {
var tooManyRequests = !bucket.consume(1);

// set throttle headers after consume which changes the remaining tokens
if (options.setHeaders) {
res.header('X-RateLimit-Remaining', Math.floor(bucket.tokens));
res.header('X-RateLimit-Limit', burst);
res.header('X-RateLimit-Rate', rate);
}

if (tooManyRequests) {
req.log.info({
address: req.connection.remoteAddress || '?',
method: req.method,
Expand Down
108 changes: 81 additions & 27 deletions test/plugins/throttle.test.js
Expand Up @@ -15,25 +15,45 @@ var SERVER;

var errorMessage = 'Error message should include rate 0.5 r/s. Received: ';

///--- Tests
function setupClientServer(ip, throttleOptions, done) {
var server = restify.createServer({
dtrace: helper.dtrace,
log: helper.getLog('server')
});

describe('throttle plugin', function () {
server.use(function ghettoAuthenticate(req, res, next) {
if (req.params.name) {
req.username = req.params.name;
}

before(function setup(done) {
SERVER = restify.createServer({
next();
});

server.use(restify.plugins.throttle(throttleOptions));

server.get('/test/:name', function (req, res, next) {
res.send();
next();
});

server.listen(PORT, ip, function () {
PORT = server.address().port;
var client = restifyClients.createJsonClient({
url: 'http://' + ip + ':' + PORT,
dtrace: helper.dtrace,
log: helper.getLog('server')
retry: false
});

SERVER.use(function ghettoAuthenticate(req, res, next) {
if (req.params.name) {
req.username = req.params.name;
}
done(client, server);
});
}

next();
});
///--- Tests

describe('throttle plugin', function () {

SERVER.use(restify.plugins.throttle({
before(function setup(done) {
setupClientServer('127.0.0.1', {
burst: 1,
rate: 0.5,
username: true,
Expand All @@ -47,21 +67,9 @@ describe('throttle plugin', function () {
rate: 1
}
}
}));

SERVER.get('/test/:name', function (req, res, next) {
res.send();
next();
});

SERVER.listen(PORT, '127.0.0.1', function () {
PORT = SERVER.address().port;
CLIENT = restifyClients.createJsonClient({
url: 'http://127.0.0.1:' + PORT,
dtrace: helper.dtrace,
retry: false
});

}, function setupGlobal(client, server) {
CLIENT = client;
SERVER = server;
done();
});
});
Expand Down Expand Up @@ -170,4 +178,50 @@ describe('throttle plugin', function () {
});
});
});

it('should not expose rate limit headers per default', function (done) {
CLIENT.get('/test/throttleMe', function (err, _, res) {
assert.isUndefined(res.headers['x-ratelimit-limit']);
assert.isUndefined(res.headers['x-ratelimit-rate']);
assert.isUndefined(res.headers['x-ratelimit-rate']);

done();
});
});

it('should expose headers on options set', function (done) {
// setup a new server with headers set to true since we cant
// change throttle options after init
setupClientServer('127.0.0.2', {
burst: 17,
rate: 0.1,
username: true,
setHeaders: true
}, function setupWithHeaders (client, server) {
client.get('/test/throttleMe', function (err, req, res) {
assert.equal(res.headers['x-ratelimit-limit'], '17');
assert.equal(res.headers['x-ratelimit-rate'], '0.1');
assert.equal(res.headers['x-ratelimit-remaining'], '16');

// it should count down
client.get(
'/test/throttleMe',
function (nextErr, nextReq, nextRes) {
assert.equal(
nextRes.headers['x-ratelimit-limit'], '17'
);
assert.equal(
nextRes.headers['x-ratelimit-rate'], '0.1'
);
assert.equal(
nextRes.headers['x-ratelimit-remaining'], '15'
);

client.close();
server.close(done);
}
);
});
});
});
});