express-throttle
Advanced tools
Comparing version 1.2.0 to 1.3.0
@@ -14,13 +14,13 @@ "use strict"; | ||
if (opts.rate.fixed) { | ||
var period = opts.rate.period; | ||
refill = function(client, dt) { | ||
// Accumulated delta times | ||
var t = (client.t || 0) + dt; | ||
refill = function(bucket, t) { | ||
bucket.window_start = bucket.window_start || t; | ||
if (t >= period) { | ||
client.t = t % period; | ||
return burst; | ||
var bucket1 = Math.floor((bucket.mtime - bucket.window_start) / bucket.period); | ||
var bucket2 = Math.floor((t - bucket.window_start) / bucket.period); | ||
if (bucket1 == bucket2) { | ||
return 0; | ||
} else { | ||
client.t = t; | ||
return 0; | ||
bucket.window_start = t; | ||
return bucket.size; | ||
} | ||
@@ -30,8 +30,8 @@ }; | ||
var rate = opts.rate.amount / opts.rate.period; | ||
refill = function(client, dt) { | ||
return rate * dt; | ||
}; | ||
refill = function(bucket, t) { | ||
return rate * (t - bucket.mtime); | ||
}; | ||
} | ||
var burst = opts.burst || opts.rate.amount; | ||
var bucket_size = opts.burst || opts.rate.amount; | ||
var store = opts.store || new MemoryStore(10000); | ||
@@ -55,3 +55,7 @@ | ||
var on_throttled = opts.on_throttled || function(req, res) { | ||
var on_allowed = opts.on_allowed || function(req, res, next, bucket) { // eslint-disable-line no-unused-vars | ||
next(); | ||
}; | ||
var on_throttled = opts.on_throttled || function(req, res, next, bucket) { // eslint-disable-line no-unused-vars | ||
res.status(429).end(); | ||
@@ -64,3 +68,3 @@ }; | ||
store.get(key, function(err, client) { | ||
store.get(key, function(err, bucket) { | ||
if (err) { | ||
@@ -70,5 +74,7 @@ return next(err); | ||
client = client || { "tokens": burst }; | ||
var passthrough = update_tokens(client, refill, burst, cost); | ||
store.set(key, client, function(err) { | ||
var t = Date.now(); | ||
bucket = bucket || create_bucket(bucket_size, opts.rate.period, t); | ||
var is_allowed = update_bucket(bucket, refill, cost, t); | ||
store.set(key, bucket, function(err) { | ||
if (err) { | ||
@@ -78,6 +84,6 @@ return next(err); | ||
if (passthrough) { | ||
next(); | ||
if (is_allowed) { | ||
on_allowed(req, res, next, bucket); | ||
} else { | ||
on_throttled(req, res); | ||
on_throttled(req, res, next, bucket); | ||
} | ||
@@ -89,12 +95,2 @@ }); | ||
function shallow_clone(obj) { | ||
var clone = {}; | ||
for (var key in obj) { | ||
clone[key] = obj[key]; | ||
} | ||
return clone; | ||
} | ||
function parse_options(options) { | ||
@@ -128,2 +124,6 @@ if (typeof(options) == "string") { | ||
} | ||
if (options.on_allowed && typeof(options.on_allowed) != "function") { | ||
throw new Error("'on_allowed' needs to be a function."); | ||
} | ||
@@ -137,2 +137,12 @@ if (options.on_throttled && typeof(options.on_throttled) != "function") { | ||
function shallow_clone(obj) { | ||
var clone = {}; | ||
for (var key in obj) { | ||
clone[key] = obj[key]; | ||
} | ||
return clone; | ||
} | ||
var RATE_PATTERN = /^(\d+)\/(\d+)?(ms|s|sec|second|m|min|minute|h|hour|d|day)(:fixed)?$/; | ||
@@ -174,13 +184,26 @@ | ||
function update_tokens(client, refill, burst, cost) { | ||
var t = Date.now(); | ||
var dt = t - (client.accessed || t); | ||
function create_bucket(size, period, ctime) { | ||
return { | ||
// max tokens this bucket will have (static) | ||
"size": size, | ||
// time in ms it takes to replenish all tokens (static) | ||
"period": period, | ||
// current token count | ||
"tokens": size, | ||
// last modification time | ||
"mtime": ctime, | ||
// reset time (time left in this period) | ||
"rtime": ctime + period | ||
}; | ||
} | ||
function update_bucket(bucket, refill, cost, t) { | ||
// Apply the refill first so it doesn't cancel out with the tokens we are | ||
// about to consume | ||
client.tokens = clamp_max(client.tokens + refill(client, dt), burst); | ||
client.accessed = t; | ||
// about to drain | ||
bucket.tokens = clamp_max(bucket.tokens + refill(bucket, t), bucket.size); | ||
bucket.mtime = t; | ||
bucket.rtime = Math.abs(bucket.period - t % bucket.period); | ||
if (client.tokens >= cost) { | ||
client.tokens -= cost; | ||
if (bucket.tokens >= cost) { | ||
bucket.tokens -= cost; | ||
return true; | ||
@@ -187,0 +210,0 @@ } else { |
{ | ||
"name": "express-throttle", | ||
"version": "1.2.0", | ||
"version": "1.3.0", | ||
"description": "Request throttling middleware for Express", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -47,3 +47,3 @@ # express-throttle | ||
// "Half" requests are supported as well | ||
// "Half" requests are supported as well (1 request every other second) | ||
app.post("/search", throttle({ "rate": "1/2s", "burst": 5 }), function(req, res, next) { | ||
@@ -105,4 +105,3 @@ // ... | ||
"rate": "5/s", | ||
"burst": 10, | ||
"on_throttled": function(req, res) { | ||
"on_throttled": function(req, res, next, bucket) { | ||
// Possible course of actions: | ||
@@ -113,2 +112,3 @@ // 1) Log request | ||
res.set("X-Rate-Limit-Limit", 5); | ||
res.set("X-Rate-Limit-Remaining", 0); | ||
res.status(503).send("System overloaded, try again at a later time."); | ||
@@ -118,2 +118,13 @@ } | ||
``` | ||
You may also customize the response on requests that are passed through: | ||
```js | ||
var options = { | ||
"rate": "5/s", | ||
"on_allowed": function(req, res, next, bucket) { | ||
res.set("X-Rate-Limit-Limit", 5); | ||
res.set("X-Rate-Limit-Remaining", bucket.tokens); | ||
res.set("X-Rate-Limit-Reset", bucket.rtime); | ||
} | ||
} | ||
``` | ||
Throttling can be applied across multiple processes. This requires an external storage mechanism which can be configured as follows: | ||
@@ -127,10 +138,10 @@ ```js | ||
ExternalStorage.prototype.get = function(key, callback) { | ||
fetch(key, function(entry) { | ||
fetch(key, function(bucket) { | ||
// First argument should be null if no errors occurred | ||
callback(null, entry); | ||
callback(null, bucket); | ||
}); | ||
} | ||
ExternalStorage.prototype.set = function(key, value, callback) { | ||
save(key, value, function(err) { | ||
ExternalStorage.prototype.set = function(key, bucket, callback) { | ||
save(key, bucket, function(err) { | ||
// err should be null if no errors occurred | ||
@@ -166,11 +177,19 @@ callback(err); | ||
function get(key, callback) { | ||
fetch(key, function(err, value) { | ||
fetch(key, function(err, bucket) { | ||
if (err) callback(err); | ||
else callback(null, value); | ||
else callback(null, bucket); | ||
}); | ||
} | ||
function set(key, value, callback) { | ||
// value will be an object with the following structure: | ||
// { "tokens": Number, "accessed": Number } | ||
save(key, value, function(err) { | ||
function set(key, bucket, callback) { | ||
// 'bucket' will be an object with the following structure: | ||
/* | ||
{ | ||
"size": Number, (max number of tokens this bucket can have) | ||
"period": Number, (time in ms it takes to replenish all tokens) | ||
"tokens": Number, (current number of tokens) | ||
"mtime": Number, (last modification time) | ||
"rtime": Number (time until next reset) | ||
} | ||
*/ | ||
save(key, bucket, function(err) { | ||
callback(err); | ||
@@ -189,9 +208,16 @@ } | ||
`cost`: Number or function used to calculate the cost for a request. It will be called with an [express request object](http://expressjs.com/en/4x/api.html#req). Defaults to 1. | ||
`cost`: Number or function used to calculate the cost for a request with an [express request object](http://expressjs.com/en/4x/api.html#req). Defaults to 1. | ||
`on_throttled`: A function called when the request is throttled. It will be called with an [express request object](http://expressjs.com/en/4x/api.html#req) and [express response object](http://expressjs.com/en/4x/api.html#res). Defaults to: | ||
`on_allowed`: A function called when the request is passed through with an [express request object](http://expressjs.com/en/4x/api.html#req), [express response object](http://expressjs.com/en/4x/api.html#res), `next` function and a `bucket` object. Defaults to: | ||
```js | ||
function(req, res) { | ||
function(req, res, next, bucket) { | ||
next(); | ||
} | ||
``` | ||
`on_throttled`: A function called when the request is throttled with an [express request object](http://expressjs.com/en/4x/api.html#req), [express response object](http://expressjs.com/en/4x/api.html#res), `next` function and a `bucket` object. Defaults to: | ||
```js | ||
function(req, res, next, bucket) { | ||
res.status(429).end(); | ||
}; | ||
``` |
@@ -11,3 +11,3 @@ "use strict"; | ||
function close_to(value, target, delta = 0.001) { | ||
return Math.abs(value - target) < delta; | ||
return Math.abs(value - target) <= delta; | ||
} | ||
@@ -62,3 +62,3 @@ | ||
t.test("...with 'key' not being a function", st => { | ||
st.throws(() => throttle({ "rate": "1/s", "burst": 5, "key": 1 }), new Error); | ||
st.throws(() => throttle({ "rate": "1/s", "key": 1 }), new Error); | ||
st.end(); | ||
@@ -68,8 +68,13 @@ }); | ||
t.test("...with 'cost' not being a number or function", st => { | ||
st.throws(() => throttle({ "rate": "1/s", "burst": 5, "cost": "5" }), new Error); | ||
st.throws(() => throttle({ "rate": "1/s", "cost": "5" }), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with 'on_allowed' not being a function", st => { | ||
st.throws(() => throttle({ "rate": "1/s", "on_allowed": "test" }), new Error); | ||
st.end(); | ||
}); | ||
t.test("...with 'on_throttled' not being a function", st => { | ||
st.throws(() => throttle({ "rate": "1/s", "burst": 5, "on_throttled": "test" }), new Error); | ||
st.throws(() => throttle({ "rate": "1/s", "on_throttled": "test" }), new Error); | ||
st.end(); | ||
@@ -102,2 +107,3 @@ }); | ||
"cost": () => true, | ||
"on_allowed": () => true, | ||
"on_throttled": () => true | ||
@@ -223,4 +229,4 @@ })); | ||
st.equal(res.status, 200); | ||
store.get(res.body, (err, entry) => { | ||
st.ok(entry); | ||
store.get(res.body, (err, bucket) => { | ||
st.ok(bucket); | ||
st.end(); | ||
@@ -239,4 +245,4 @@ }); | ||
t.equal(res.status, 200); | ||
store.get(proxy_ip, (err, entry) => { | ||
t.ok(entry); | ||
store.get(proxy_ip, (err, bucket) => { | ||
t.ok(bucket); | ||
t.end(); | ||
@@ -258,4 +264,4 @@ }); | ||
t.equal(res.status, 200); | ||
store.get(custom_key, (err, entry) => { | ||
t.ok(entry); | ||
store.get(custom_key, (err, bucket) => { | ||
t.ok(bucket); | ||
t.end(); | ||
@@ -276,5 +282,5 @@ }); | ||
request(app).get("/").end((err, res) => { | ||
store.get(res.body, (err, entry) => { | ||
store.get(res.body, (err, bucket) => { | ||
t.equal(res.status, 200); | ||
t.assert(close_to(entry.tokens, 2)); | ||
t.assert(close_to(bucket.tokens, 2)); | ||
@@ -309,10 +315,10 @@ request(app).get("/").end((err, res) => { | ||
request(app).get("/yes").end((err, res) => { | ||
store.get(res.body, (err, entry) => { | ||
store.get(res.body, (err, bucket) => { | ||
t.equal(res.status, 200); | ||
t.assert(close_to(entry.tokens, 5)); | ||
t.assert(close_to(bucket.tokens, 5)); | ||
request(app).get("/no").end((err, res) => { | ||
store.get(res.body, (err, entry) => { | ||
store.get(res.body, (err, bucket) => { | ||
t.equal(res.status, 200); | ||
t.assert(close_to(entry.tokens, 2)); | ||
t.assert(close_to(bucket.tokens, 2)); | ||
@@ -329,7 +335,22 @@ request(app).get("/no").end((err, res) => { | ||
test("custom on_allowed function", t => { | ||
var app = create_app({ | ||
"rate": "1/s", | ||
"on_allowed": function(req, res, next, bucket) { | ||
res.status(201).json(bucket); | ||
} | ||
}); | ||
request(app).get("/").end((err, res) => { | ||
t.equal(res.status, 201); | ||
t.assert(close_to(res.body.tokens, 0)); | ||
t.end(); | ||
}); | ||
}); | ||
test("custom on_throttled function", t => { | ||
var app = create_app({ | ||
"rate": "1/s", | ||
"on_throttled": function(req, res) { | ||
res.status(503).json("slow down!"); | ||
"on_throttled": function(req, res, next, bucket) { | ||
res.status(503).json(bucket); | ||
} | ||
@@ -341,5 +362,5 @@ }); | ||
t.equal(res.status, 503); | ||
t.equal(res.body, "slow down!"); | ||
t.assert(close_to(res.body.tokens, 0)); | ||
t.end(); | ||
}); | ||
}); |
@@ -11,8 +11,2 @@ Multiple rate limits: Achieved by multiple throttles | ||
Staggering windows | ||
Fixed refresh rate | ||
Support for ms | ||
Change cache size | ||
@@ -19,0 +13,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
25496
495
216