express-throttle
Advanced tools
Comparing version 1.4.0 to 2.0.0
@@ -13,4 +13,4 @@ "use strict"; | ||
MemoryStore.prototype.set = function(key, bucket, lifetime, callback) { | ||
this.cache.set(key, bucket, lifetime); | ||
MemoryStore.prototype.set = function(key, bucket, callback) { | ||
this.cache.set(key, bucket); | ||
callback(); | ||
@@ -17,0 +17,0 @@ }; |
"use strict"; | ||
var MemoryStore = require("./memory-store"); | ||
exports.parse = function parse_options(options) { | ||
if (typeof(options) == "string") { | ||
options = { "rate": options }; | ||
} | ||
if (typeof(options) != "object") { | ||
@@ -13,27 +11,78 @@ throw new Error("options needs to be an object."); | ||
} | ||
if (options.store_size && typeof(options.store_size) != "number") { | ||
throw new Error("'store_size' needs to be a number."); | ||
} | ||
if (typeof(options.rate) != "string") { | ||
throw new Error("'rate' needs to be a string (e.g 3/s, 5/2min, 10/day)."); | ||
} | ||
options.store = options.store || new MemoryStore(options.store_size || 10000); | ||
options.rate = parse_rate(options.rate); | ||
if (options.rate) { | ||
if (typeof(options.rate) != "string") { | ||
throw new Error("'rate' needs to be a string (e.g 3/s, 5/2min, 10/day)."); | ||
} | ||
if (options.burst && typeof(options.burst) != "number") { | ||
throw new Error("'burst' needs to be a number."); | ||
options.rate = parse_rate(options.rate); | ||
if (options.burst) { | ||
if (typeof(options.burst) != "number") { | ||
throw new Error("'burst' needs to be a number."); | ||
} | ||
} else { | ||
options.burst = options.rate.amount; | ||
} | ||
add_methods_rolling(options); | ||
} else if (options.period) { | ||
if (typeof(options.period) != "string") { | ||
throw new Error("'period' needs to be a string (e.g 2h, second, 5min)."); | ||
} | ||
options.period = parse_period(options.period); | ||
if (typeof(options.burst) != "number") { | ||
throw new Error("'burst' needs to be a number."); | ||
} | ||
add_methods_fixed(options); | ||
} else { | ||
throw new Error("Either 'rate' or 'period' must be supplied."); | ||
} | ||
if (options.key && typeof(options.key) != "function") { | ||
throw new Error("'key' needs to be a function."); | ||
if (options.key) { | ||
if (typeof(options.key) != "function") { | ||
throw new Error("'key' needs to be a function."); | ||
} | ||
} else { | ||
options.key = function(req) { return req.ip; } | ||
} | ||
if (options.cost && !(typeof(options.cost) == "number" || typeof(options.cost) == "function")) { | ||
throw new Error("'cost' needs to be a number or function."); | ||
if (options.cost) { | ||
if (typeof(options.cost) == "number") { | ||
var cost = options.cost; | ||
options.cost = function() { return cost; } | ||
} else if (typeof(options.cost) != "function") { | ||
throw new Error("'cost' needs to be a number or function."); | ||
} | ||
} else { | ||
options.cost = function() { return 1; } | ||
} | ||
if (options.on_allowed && typeof(options.on_allowed) != "function") { | ||
throw new Error("'on_allowed' needs to be a function."); | ||
if (options.on_allowed) { | ||
if (typeof(options.on_allowed) != "function") { | ||
throw new Error("'on_allowed' needs to be a function."); | ||
} | ||
} else { | ||
options.on_allowed = function(req, res, next, bucket) { // eslint-disable-line no-unused-vars | ||
next(); | ||
}; | ||
} | ||
if (options.on_throttled && typeof(options.on_throttled) != "function") { | ||
throw new Error("'on_throttled' needs to be a function."); | ||
if (options.on_throttled) { | ||
if (typeof(options.on_throttled) != "function") { | ||
throw new Error("'on_throttled' needs to be a function."); | ||
} | ||
} else { | ||
options.on_throttled = function(req, res, next, bucket) { // eslint-disable-line no-unused-vars | ||
res.status(429).end(); | ||
}; | ||
} | ||
@@ -54,3 +103,3 @@ | ||
var RATE_PATTERN = /^(\d+)\/(\d+)?(ms|s|sec|second|m|min|minute|h|hour|d|day)(:fixed)?$/; | ||
var RATE_PATTERN = /^(\d+)\/(\d+)?(ms|s|sec|m|min|h|hour|d|day)$/; | ||
@@ -61,3 +110,3 @@ function parse_rate(rate) { | ||
if (!parsed_rate) { | ||
throw new Error("invalid rate (e.g 3/s, 5/2min, 10/day)."); | ||
throw new Error("invalid rate format (e.g 3/s, 5/2min, 10/day)."); | ||
} | ||
@@ -67,12 +116,35 @@ | ||
var denominator = parseInt(parsed_rate[2] || 1, 10); | ||
if (denominator == 0) { | ||
throw new Error("invalid rate denominator (can't be 0)."); | ||
} | ||
var time_unit = parsed_rate[3]; | ||
var fixed = parsed_rate[4] == ":fixed"; | ||
return { | ||
"amount": numerator, | ||
"period": denominator * time_unit_to_ms(time_unit), | ||
"fixed": fixed | ||
"period": denominator * time_unit_to_ms(time_unit) | ||
}; | ||
} | ||
var PERIOD_PATTERN = /^(\d+)?(ms|s|sec|m|min|h|hour|d|day)$/; | ||
function parse_period(period) { | ||
var parsed_period = period.match(PERIOD_PATTERN); | ||
if (!parsed_period) { | ||
throw new Error("invalid period (e.g d, 2m, 3h)") | ||
} | ||
var amount = parseInt(parsed_period[1], 10); | ||
if (amount == 0) { | ||
throw new Error("invalid period (can't be 0)."); | ||
} | ||
var time_unit = parsed_period[2]; | ||
return amount * time_unit_to_ms(time_unit); | ||
} | ||
function time_unit_to_ms(time_unit) { | ||
@@ -82,5 +154,5 @@ switch (time_unit) { | ||
return 1; | ||
case "s": case "sec": case "second": | ||
case "s": case "sec": | ||
return 1000; | ||
case "m": case "min": case "minute": | ||
case "m": case "min": | ||
return 60 * 1000; | ||
@@ -93,1 +165,36 @@ case "h": case "hour": | ||
} | ||
function add_methods_rolling(options) { | ||
options.create_bucket = function(ctime) { | ||
return { | ||
"tokens": options.burst, | ||
"mtime": ctime // last modification time | ||
}; | ||
}; | ||
var rate = options.rate.amount / options.rate.period; | ||
options.refill_bucket = function(t, bucket) { | ||
return rate * (t - bucket.mtime); | ||
} | ||
} | ||
function add_methods_fixed(options) { | ||
var burst = options.burst; | ||
var period = options.period; | ||
options.create_bucket = function(ctime) { | ||
return { | ||
"tokens": burst, | ||
"mtime": ctime, | ||
"etime": ctime + period // expiration time | ||
}; | ||
} | ||
options.refill_bucket = function(t, bucket) { | ||
if (t > bucket.etime) { | ||
bucket.etime = t + period; | ||
return burst; | ||
} else { | ||
return 0; | ||
} | ||
} | ||
} |
@@ -8,56 +8,10 @@ "use strict"; | ||
// Default token storage (memory-bounded LRU cache) | ||
var MemoryStore = require("./memory-store"); | ||
function Throttle(opts) { | ||
opts = options.parse(opts); | ||
var refill; | ||
if (opts.rate.fixed) { | ||
refill = function(bucket, t) { // eslint-disable-line no-unused-vars | ||
// We are not refilling a bucket during the course of its | ||
// lifetime. A new bucket is created when the old one expires. | ||
return 0; | ||
}; | ||
} else { | ||
var rate = opts.rate.amount / opts.rate.period; | ||
refill = function(bucket, t) { | ||
return rate * (t - bucket.mtime); | ||
}; | ||
} | ||
var bucket_settings = { | ||
"size": opts.burst || opts.rate.amount, | ||
"period": opts.rate.period, | ||
"refill": refill | ||
}; | ||
var store = opts.store || new MemoryStore(10000); | ||
// key function, used to identify the client we are going to throttle | ||
var key_func = opts.key || function(req) { return req.ip; }; | ||
var cost_func; | ||
// cost function, calculates the number of tokens to be subtracted per request | ||
if (typeof(opts.cost) == "number") { | ||
cost_func = function() { return opts.cost; }; | ||
} else if (typeof(opts.cost) == "function") { | ||
cost_func = opts.cost; | ||
} else { | ||
cost_func = function() { return 1; }; | ||
} | ||
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(); | ||
}; | ||
return function(req, res, next) { | ||
var key = key_func(req); | ||
var cost = cost_func(req); | ||
var key = opts.key(req); | ||
var cost = opts.cost(req); | ||
store.get(key, function(err, bucket) { | ||
opts.store.get(key, function(err, bucket) { | ||
if (err) { | ||
@@ -68,6 +22,9 @@ return next(err); | ||
var t = Date.now(); | ||
bucket = bucket || create_bucket(bucket_settings, t); | ||
var is_allowed = update_bucket(bucket, bucket_settings, cost, t); | ||
bucket = bucket || opts.create_bucket(t); | ||
var tokens = opts.refill_bucket(t, bucket); | ||
bucket.tokens = clamp_max(bucket.tokens + tokens, opts.burst); | ||
var is_allowed = drain_tokens(bucket, cost); | ||
bucket.mtime = t; | ||
store.set(key, bucket, bucket_settings.period, function(err) { | ||
opts.store.set(key, bucket, function(err) { | ||
if (err) { | ||
@@ -78,5 +35,5 @@ return next(err); | ||
if (is_allowed) { | ||
on_allowed(req, res, next, bucket); | ||
opts.on_allowed(req, res, next, bucket); | ||
} else { | ||
on_throttled(req, res, next, bucket); | ||
opts.on_throttled(req, res, next, bucket); | ||
} | ||
@@ -88,21 +45,3 @@ }); | ||
function create_bucket(settings, ctime) { | ||
return { | ||
// current token count | ||
"tokens": settings.size, | ||
// creation time | ||
"ctime": ctime, | ||
// last modification time | ||
"mtime": ctime, | ||
// expiration time | ||
"etime": ctime + settings.period | ||
}; | ||
} | ||
function update_bucket(bucket, settings, cost, t) { | ||
// Apply the refill first so it doesn't cancel out with the tokens we are | ||
// about to drain | ||
bucket.tokens = clamp_max(bucket.tokens + settings.refill(bucket, t), settings.size); | ||
bucket.mtime = t; | ||
function drain_tokens(bucket, cost) { | ||
if (bucket.tokens >= cost) { | ||
@@ -109,0 +48,0 @@ bucket.tokens -= cost; |
{ | ||
"name": "express-throttle", | ||
"version": "1.4.0", | ||
"version": "2.0.0", | ||
"description": "Request throttling middleware for Express", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -34,15 +34,15 @@ # express-throttle | ||
// Throttle to 5 reqs/s | ||
app.post("/search", throttle("5/s"), function(req, res, next) { | ||
// Allow 5 requests at any rate with an average rate of 5/s | ||
app.post("/search", throttle({ "rate": "5/s" }), function(req, res, next) { | ||
// ... | ||
}); | ||
// ...using fixed time windows instead | ||
app.post("/search", throttle("5/s:fixed"), function(req, res, next) { | ||
// Allow 5 requests at any rate during a fixed time window of 1 sec | ||
app.post("/search", throttle({ "burst": 5, "period": "1s" }), function(req, res, next) { | ||
// ... | ||
}) | ||
}); | ||
``` | ||
Combine it with a burst capacity of 10, meaning that the client can make 10 requests at any rate. The capacity is "refilled" with the specified rate (in this case 5/s). | ||
Combine it with a burst capacity of 10, meaning that the client can make 10 requests at any rate. The burst capacity is "refilled" with the specified rate (in this case 5/s). | ||
```js | ||
app.post("/search", throttle({ "rate": "5/s", "burst": 10 }), function(req, res, next) { | ||
app.post("/search", throttle({ "burst": 10, "rate": "5/s" }), function(req, res, next) { | ||
// ... | ||
@@ -52,3 +52,3 @@ }); | ||
// "Half" requests are supported as well (1 request every other second) | ||
app.post("/search", throttle({ "rate": "1/2s", "burst": 5 }), function(req, res, next) { | ||
app.post("/search", throttle({ "burst": 5, "rate": "1/2s" }), function(req, res, next) { | ||
// ... | ||
@@ -60,4 +60,4 @@ }); | ||
var options = { | ||
"burst": 10, | ||
"rate": "5/s", | ||
"burst": 10, | ||
"key": function(req) { | ||
@@ -77,4 +77,4 @@ return req.session.username; | ||
var options = { | ||
"burst": 10, | ||
"rate": "5/s", | ||
"burst": 10, | ||
"cost": function(req) { | ||
@@ -110,3 +110,4 @@ var ip_address = req.connection.remoteAddress; | ||
var options = { | ||
"rate": "5/s", | ||
"burst": 5 | ||
"period": "1min", | ||
"on_throttled": function(req, res, next, bucket) { | ||
@@ -119,3 +120,4 @@ // Possible course of actions: | ||
res.set("X-Rate-Limit-Remaining", 0); | ||
// bucket.etime = expiration time in Unix epoch ms | ||
// bucket.etime = expiration time in Unix epoch ms, only available | ||
// for fixed time windows | ||
res.set("X-Rate-Limit-Reset", bucket.etime); | ||
@@ -133,3 +135,2 @@ res.status(503).send("System overloaded, try again at a later time."); | ||
res.set("X-Rate-Limit-Remaining", bucket.tokens); | ||
res.set("X-Rate-Limit-Reset", bucket.etime); | ||
} | ||
@@ -171,10 +172,14 @@ } | ||
`rate`: Determines the number of requests allowed within the specified time before subsequent requests get throttled. Must be specified according to the following format: *X/Yt(:fixed)* | ||
`burst`: The number of requests that can be made at any rate. Defaults to *X* as defined below for rolling windows. | ||
where *X* and *Y* are integers and *t* is the time unit which can be any of the following: `ms, s, sec, second, m, min, minute, h, hour, d, day` | ||
`rate`: Determines the rate at which the burst quota is "refilled". This will control the average number of requests per time unit. Must be specified according to the following format: *X/Yt* | ||
If you prefer tokens to be refilled in fixed intervals, append `:fixed`. E.g `5/min:fixed`. | ||
where *X* and *Y* are integers and *t* is the time unit which can be any of the following: `ms, s, sec, m, min, h, hour, d, day` | ||
`burst`: The number of requests that can be made at any rate. Defaults to *X* as defined above. | ||
E.g `5/s, 180/15min, 1000/d` | ||
`period`: The duration of the time window after which the entire burst quota is refilled. Must be specified according to the following format: *Y/t*, where *Y* and *t* are defined as above. | ||
E.g `5s, 15min, 1000d` | ||
`store`: Custom storage class. Must implement a `get` and `set` method with the following signatures: | ||
@@ -190,4 +195,3 @@ ```js | ||
} | ||
function set(key, bucket, lifetime, callback) { | ||
// lifetime (in ms) - same as 'Y' as defined above multiplied by the time unit | ||
function set(key, bucket, callback) { | ||
// 'bucket' will be an object with the following structure: | ||
@@ -197,5 +201,4 @@ /* | ||
"tokens": Number, (current number of tokens) | ||
"ctime": Number, (creation time) | ||
"mtime": Number, (last modification time) | ||
"etime": Number (expiration time) | ||
"etime": Number (expiration time, only available for fixed time windows) | ||
} | ||
@@ -210,2 +213,4 @@ */ | ||
`store_size`: Determines the maximum number of entries for the default in-memory LRU cache. 0 indicates no limit, **not recommended**, since entries in the cache are not expired / cleaned up / garbage collected. | ||
`key`: Function used to identify clients. It will be called with an [express request object](http://expressjs.com/en/4x/api.html#req). Defaults to: | ||
@@ -232,6 +237,2 @@ ```js | ||
}; | ||
``` | ||
## Benchmark | ||
Refer to [this](https://github.com/GlurG/express-throttle/blob/master/Benchmark.md) document for ballparks / guidelines of the performance penalty this middleware will incur. | ||
``` |
"use strict"; | ||
var tap = require("tap"); | ||
var throttle = require("../lib/throttle"); | ||
var options = require("../lib/options"); | ||
tap.test("fail to init...", function(t) { | ||
t.test("...without options", function(st) { | ||
st.throws(throttle); | ||
function wrap(opts) { | ||
return function() { | ||
options.parse(opts); | ||
} | ||
} | ||
tap.test("no options", function(t) { | ||
t.throws(wrap()); | ||
t.end(); | ||
}); | ||
tap.test("options not being an object", function(t) { | ||
t.throws(wrap(5)); | ||
t.end(); | ||
}); | ||
tap.test("neither rate nor period specified", function(t) { | ||
t.throws(wrap({})); | ||
t.end(); | ||
}); | ||
tap.test("store_size not being a number", function(t) { | ||
t.throws(wrap({ "store_size": "5" })); | ||
t.end(); | ||
}); | ||
tap.test("invalid rate...", function(t) { | ||
tap.test("not being a string", function(st) { | ||
st.throws(wrap({ "rate": 5 })); | ||
st.end(); | ||
}); | ||
t.test("...with first argument not being a string or object", function(st) { | ||
st.throws(function() { throttle(5); }); | ||
t.test("amount not being a number", function(st) { | ||
st.throws(wrap({ "rate": "a/m" })); | ||
st.end(); | ||
}); | ||
t.test("...with invalid rate string (float not allowed)", function(st) { | ||
st.throws(function() { throttle("1.0/h"); }); | ||
t.test("float amount not allowed", function(st) { | ||
st.throws(wrap({ "rate": "1.0/m" })); | ||
st.end(); | ||
}); | ||
t.test("...with invalid rate string (float not allowed)", function(st) { | ||
st.throws(function() { throttle("1/2.0h"); }); | ||
t.test("negative amount not allowed", function(st) { | ||
st.throws(wrap({ "rate": "-1/m" })); | ||
st.end(); | ||
}); | ||
t.test("...with invalid rate option", function(st) { | ||
st.throws(function() { throttle("10/m:test"); }); | ||
t.test("invalid period", function(st) { | ||
st.throws(wrap({ "rate": "1/a" })); | ||
st.end(); | ||
}); | ||
t.test("...with empty option object", function(st) { | ||
st.throws(function() { throttle({}); }); | ||
t.test("case sensitive time unit", function(st) { | ||
st.throws(wrap({ "rate": "1/M" })); | ||
st.end(); | ||
}); | ||
t.test("...with 'burst' not being a number", function(st) { | ||
st.throws(function() { throttle({ "rate": "1/s", "burst": "5" }); }); | ||
t.test("float period not allowed", function(st) { | ||
st.throws(wrap({ "rate": "1/2.0m" })); | ||
st.end(); | ||
}); | ||
t.test("...with 'key' not being a function", function(st) { | ||
st.throws(function() { throttle({ "rate": "1/s", "key": 1 }); }); | ||
t.test("negative period not allowed", function(st) { | ||
st.throws(wrap({ "rate": "1/-2m" })); | ||
st.end(); | ||
}); | ||
t.test("...with 'cost' not being a number or function", function(st) { | ||
st.throws(function() { throttle({ "rate": "1/s", "cost": "5" }); }); | ||
t.test("period can't be 0", function(st) { | ||
st.throws(wrap({ "rate": "1/0m" })); | ||
st.end(); | ||
}); | ||
t.end(); | ||
}); | ||
t.test("...with 'on_allowed' not being a function", function(st) { | ||
st.throws(function() { throttle({ "rate": "1/s", "on_allowed": "test" }); }); | ||
tap.test("valid rate...", function(t) { | ||
t.test("with only time unit", function(st) { | ||
st.doesNotThrow(wrap({ "rate": "1/s" })); | ||
st.end(); | ||
}); | ||
t.test("...with 'on_throttled' not being a function", function(st) { | ||
st.throws(function() { throttle({ "rate": "1/s", "on_throttled": "test" }); }); | ||
t.test("with denominator + time unit", function(st) { | ||
st.doesNotThrow(wrap({ "rate": "3/2s" })); | ||
st.end(); | ||
@@ -65,35 +95,136 @@ }); | ||
tap.test("init with...", function(t) { | ||
t.test("...rate", function(st) { | ||
st.doesNotThrow(function() { throttle("1/200ms"); }); | ||
st.doesNotThrow(function() { throttle("1/s"); }); | ||
st.doesNotThrow(function() { throttle("1/2sec"); }); | ||
st.doesNotThrow(function() { throttle("1/second"); }); | ||
st.doesNotThrow(function() { throttle("1/m"); }); | ||
st.doesNotThrow(function() { throttle("1/3min"); }); | ||
st.doesNotThrow(function() { throttle("1/minute"); }); | ||
st.doesNotThrow(function() { throttle("1/4h"); }); | ||
st.doesNotThrow(function() { throttle("1/hour"); }); | ||
st.doesNotThrow(function() { throttle("1/d"); }); | ||
st.doesNotThrow(function() { throttle("1/5day"); }); | ||
st.doesNotThrow(function() { throttle("1/m:fixed"); }); | ||
tap.test("rate + burst not being a number", function(t) { | ||
t.throws(wrap({ "rate": "1/s", "burst": "5" })); | ||
t.end(); | ||
}); | ||
tap.test("burst defaulting to rate.amount", function(t) { | ||
var burst = options.parse({ "rate": "5/s" }).burst; | ||
t.equal(burst, 5); | ||
t.end(); | ||
}); | ||
tap.test("invalid period...", function(t) { | ||
t.test("not being a string", function(st) { | ||
st.throws(wrap({ "period": 10 })); | ||
st.end(); | ||
}); | ||
t.test("...options object", function(st) { | ||
st.doesNotThrow(function() { | ||
throttle({ | ||
"rate": "1/s", | ||
"burst": 5, | ||
"key": function() {}, | ||
"cost": function() {}, | ||
"on_allowed": function() {}, | ||
"on_throttled": function() {} | ||
}); | ||
}); | ||
t.test("amount not being a number", function(st) { | ||
st.throws(wrap({ "period": "am" })); | ||
st.end(); | ||
}); | ||
t.test("case sensitive time unit", function(st) { | ||
st.throws(wrap({ "period": "1M" })); | ||
st.end(); | ||
}); | ||
t.test("float amount not allowed", function(st) { | ||
st.throws(wrap({ "period": "1.0m" })); | ||
st.end(); | ||
}); | ||
t.test("negative amount not allowed", function(st) { | ||
st.throws(wrap({ "period": "-1m" })); | ||
st.end(); | ||
}); | ||
t.test("amount can't be 0", function(st) { | ||
st.throws(wrap({ "period": "0m" })); | ||
st.end(); | ||
}); | ||
t.end(); | ||
}); | ||
tap.test("valid period...", function(t) { | ||
t.test("with only time unit", function(st) { | ||
st.doesNotThrow(wrap({ "burst": 1, "period": "s" })); | ||
st.end(); | ||
}); | ||
t.test("with amount + time unit", function(st) { | ||
st.doesNotThrow(wrap({ "burst": 1, "period": "2s" })); | ||
st.end(); | ||
}); | ||
t.end(); | ||
}); | ||
tap.test("only period specified", function(t) { | ||
t.throws(wrap({ "period": "10s" })); | ||
t.end(); | ||
}); | ||
tap.test("period + burst not being a number", function(t) { | ||
t.throws(wrap({ "period": "10s", "burst": "5" })); | ||
t.end(); | ||
}); | ||
tap.test("key not being a function", function(t) { | ||
t.throws(wrap({ "rate": "1/s", "key": "ip" })); | ||
t.end(); | ||
}); | ||
tap.test("cost not being a number or function", function(t) { | ||
t.throws(wrap({ "rate": "1/s", "cost": "5" })); | ||
t.end(); | ||
}); | ||
tap.test("default cost = 1", function(t) { | ||
var cost = options.parse({ "rate": "1/s" }).cost(); | ||
t.equal(cost, 1); | ||
t.end(); | ||
}); | ||
tap.test("on_allowed not being a function", function(t) { | ||
t.throws(wrap({ "rate": "1/s", "on_allowed": 5 })); | ||
t.end(); | ||
}); | ||
tap.test("on_throttled not being a function", function(t) { | ||
t.throws(wrap({ "rate": "1/s", "on_throttled": 5 })); | ||
t.end(); | ||
}); | ||
tap.test("init with all time units", function(t) { | ||
t.doesNotThrow(wrap({ "burst": 1, "period": "100ms" })); | ||
t.doesNotThrow(wrap({ "burst": 1, "period": "100s" })); | ||
t.doesNotThrow(wrap({ "burst": 1, "period": "100sec" })); | ||
t.doesNotThrow(wrap({ "burst": 1, "period": "100m" })); | ||
t.doesNotThrow(wrap({ "burst": 1, "period": "100min" })); | ||
t.doesNotThrow(wrap({ "burst": 1, "period": "100h" })); | ||
t.doesNotThrow(wrap({ "burst": 1, "period": "100hour" })); | ||
t.doesNotThrow(wrap({ "burst": 1, "period": "100d" })); | ||
t.doesNotThrow(wrap({ "burst": 1, "period": "100day" })); | ||
t.end(); | ||
}); | ||
tap.test("init with everything (rolling)", function(t) { | ||
t.doesNotThrow(wrap({ | ||
"burst": 10, | ||
"rate": "5/m", | ||
"store_size": 100, | ||
"key": function() {}, | ||
"cost": function() {}, | ||
"on_allowed": function() {}, | ||
"on_throttled": function() {} | ||
})); | ||
t.end(); | ||
}); | ||
tap.test("init with everything (fixed)", function(t) { | ||
t.doesNotThrow(wrap({ | ||
"burst": 10, | ||
"period": "5m", | ||
"store_size": 100, | ||
"key": function() {}, | ||
"cost": function() {}, | ||
"on_allowed": function() {}, | ||
"on_throttled": function() {} | ||
})); | ||
t.end(); | ||
}); |
@@ -10,8 +10,10 @@ "use strict"; | ||
function create_app() { | ||
function create_app(options) { | ||
var app = express(); | ||
options.on_allowed = function(req, res, next, bucket) { | ||
res.status(200).json(bucket); | ||
} | ||
app.get("/", throttle.apply(null, arguments), function(req, res) { | ||
res.status(200).json(req.connection.remoteAddress); | ||
}); | ||
app.get("*", throttle(options)); | ||
@@ -21,76 +23,48 @@ return app; | ||
tap.test("passthrough...", function(t) { | ||
t.plan(3); | ||
function response(t, status, tokens, callback) { | ||
return function(err, res) { | ||
t.equal(res.status, status); | ||
function verify(st, end) { | ||
return function(err, res) { | ||
st.equal(res.status, 200); | ||
if (tokens) { | ||
t.equal(Math.round(res.body.tokens), tokens); | ||
} | ||
if (end) { | ||
st.end(); | ||
} | ||
}; | ||
if (callback) { | ||
callback(); | ||
} else { | ||
t.end(); | ||
} | ||
} | ||
} | ||
t.test("...2 requests with enough gap @ rate 5/s", function(st) { | ||
var app = create_app({ "rate": "5/s", "burst": 1 }); | ||
request(app).get("/").end(verify(st)); | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
}, 250); // add 50ms to allow some margin for error | ||
}); | ||
function noop() {} | ||
t.test("...2 requests with enough gap @ rate 5/2s", function(st) { | ||
var app = create_app({ "rate": "5/2s", "burst": 1 }); | ||
request(app).get("/").end(verify(st)); | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
}, 450); | ||
}); | ||
t.test("...2 requests with enough gap @ rate 5/s:fixed", function(st) { | ||
var app = create_app({ "rate": "5/s:fixed", "burst": 1 }); | ||
request(app).get("/").end(verify(st)); | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
}, 1050); | ||
}); | ||
tap.test("rolling window", function(t) { | ||
var app = create_app({ "burst": 2, "rate": "1/100ms" }); | ||
request(app).get("/").end(response(t, 200, 1, noop)); | ||
request(app).get("/").end(response(t, 200, 0, noop)); | ||
setTimeout(function() { | ||
request(app).get("/").end(response(t, 429, 0, noop)); | ||
}, 50); | ||
setTimeout(function() { | ||
request(app).get("/").end(response(t, 200, 0, noop)); | ||
}, 120); | ||
setTimeout(function() { | ||
request(app).get("/").end(response(t, 200, 1)); | ||
}, 320); | ||
}); | ||
tap.test("throttle...", function(t) { | ||
t.plan(3); | ||
function verify(st, end) { | ||
return function(err, res) { | ||
st.equal(res.status, 429); | ||
if (end) { | ||
st.end(); | ||
} | ||
}; | ||
} | ||
t.test("...2 requests without enough gap @ rate 5/s", function(st) { | ||
var app = create_app({ "rate": "5/s", "burst": 1 }); | ||
request(app).get("/").end(function() {}); | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
}, 150); | ||
}); | ||
t.test("...2 requests without enough gap @ rate 5/2s", function(st) { | ||
var app = create_app({ "rate": "5/2s", "burst": 1 }); | ||
request(app).get("/").end(function() {}); | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
}, 350); | ||
}); | ||
t.test("...2 requests without enough gap @ rate 5/s:fixed", function(st) { | ||
var app = create_app({ "rate": "5/s:fixed", "burst": 1 }); | ||
request(app).get("/").end(function() {}); | ||
setTimeout(function() { | ||
request(app).get("/").end(verify(st, true)); | ||
}, 950); | ||
}); | ||
tap.test("fixed window", function(t) { | ||
var app = create_app({ "burst": 2, "period": "100ms" }); | ||
request(app).get("/").end(response(t, 200, 1, noop)); | ||
request(app).get("/").end(response(t, 200, 0, noop)); | ||
setTimeout(function() { | ||
request(app).get("/").end(response(t, 429, 0, noop)); | ||
}, 50); | ||
setTimeout(function() { | ||
request(app).get("/").end(response(t, 200, 1, noop)); | ||
}, 120); | ||
setTimeout(function() { | ||
request(app).get("/").end(response(t, 200, 0)); | ||
}, 140); | ||
}); | ||
@@ -100,3 +74,3 @@ | ||
t.plan(3); | ||
t.test("...that fails to retrieve", function(st) { | ||
@@ -110,3 +84,3 @@ function FailStore() { } | ||
var app = express(); | ||
app.get("/", throttle({ "rate": "1/s", "store": new FailStore() }), | ||
app.get("/", throttle({ "burst": 1, "rate": "1/s", "store": new FailStore() }), | ||
function(err, req, res, next) { // eslint-disable-line no-unused-vars | ||
@@ -123,3 +97,3 @@ st.assert(err instanceof Error); | ||
FailStore.prototype.get = function(key, callback) { callback(null, {}); }; | ||
FailStore.prototype.set = function(key, value, lifetime, callback) { | ||
FailStore.prototype.set = function(key, value, callback) { | ||
callback(new Error("failed to set")); | ||
@@ -129,3 +103,3 @@ }; | ||
var app = express(); | ||
app.get("/", throttle({ "rate": "1/s", "store": new FailStore() }), | ||
app.get("/", throttle({ "burst": 1, "rate": "1/s", "store": new FailStore() }), | ||
function(err, req, res, next) { // eslint-disable-line no-unused-vars | ||
@@ -141,10 +115,7 @@ st.assert(err instanceof Error); | ||
var store = new MemoryStore(); | ||
var app = create_app({ "rate": "1/s", "store": store }); | ||
var app = create_app({ "burst": 1, "rate": "1/s", "store": store }); | ||
request(app).get("/").end(function(err, res) { | ||
st.equal(res.status, 200); | ||
store.get(res.body, function(err, bucket) { | ||
st.ok(bucket); | ||
st.end(); | ||
}); | ||
st.end(); | ||
}); | ||
@@ -158,2 +129,3 @@ }); | ||
var app = create_app({ | ||
"burst": 1, | ||
"rate": "1/s", | ||
@@ -176,4 +148,4 @@ "store": store, | ||
var app = create_app({ | ||
"burst": 5, | ||
"rate": "1/s", | ||
"burst": 5, | ||
"store": store, | ||
@@ -184,19 +156,11 @@ "cost": 3 | ||
request(app).get("/").end(function(err, res) { | ||
store.get(res.body, function(err, bucket) { | ||
t.equal(res.status, 200); | ||
t.equal(Math.round(bucket.tokens), 2); | ||
request(app).get("/").end(function(err, res) { | ||
t.equal(res.status, 429); | ||
t.end(); | ||
}); | ||
}); | ||
t.equal(res.status, 200); | ||
t.equal(Math.round(res.body.tokens), 2); | ||
request(app).get("/").end(response(t, 429)); | ||
}); | ||
}); | ||
tap.test("custom cost function passthrough", function(t) { | ||
var app = express(); | ||
tap.test("custom cost function", function(t) { | ||
var store = new MemoryStore(); | ||
app.get("/:admin", throttle({ | ||
var app = create_app({ | ||
"burst": 5, | ||
@@ -206,45 +170,25 @@ "rate": "1/s", | ||
"cost": function(req) { | ||
if (req.params.admin == "yes") { | ||
return 0; | ||
} else { | ||
return 3; | ||
} | ||
return req.path == "/admin" ? 0 : 3; | ||
} | ||
}), function(req, res) { | ||
res.status(200).json(req.connection.remoteAddress); | ||
}); | ||
request(app).get("/yes").end(function(err, res) { | ||
store.get(res.body, function(err, bucket) { | ||
t.equal(res.status, 200); | ||
t.equal(Math.round(bucket.tokens), 5); | ||
app.get("/:admin", function(req, res) { | ||
res.status(200).end(); | ||
}); | ||
request(app).get("/no").end(function(err, res) { | ||
store.get(res.body, function(err, bucket) { | ||
t.equal(res.status, 200); | ||
t.equal(Math.round(bucket.tokens), 2); | ||
request(app).get("/no").end(function(err, res) { | ||
t.equal(res.status, 429); | ||
t.end(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
request(app).get("/admin").end(response(t, 200, 5, function() { | ||
request(app).get("/").end(response(t, 200, 2, function() { | ||
request(app).get("/").end(response(t, 429)); | ||
})); | ||
})); | ||
}); | ||
tap.test("custom on_allowed function", function(t) { | ||
var app = create_app({ | ||
"rate": "1/s", | ||
"on_allowed": function(req, res, next, bucket) { | ||
res.status(201).json(bucket); | ||
} | ||
tap.test("default on_allowed function", function(t) { | ||
var app = express(); | ||
app.get("/", throttle({ "burst": 1, "rate": "1/s" }), | ||
function(req, res, next) { // eslint-disable-line no-unused-vars | ||
res.status(200).end(); | ||
}); | ||
request(app).get("/").end(function(err, res) { | ||
t.equal(res.status, 201); | ||
t.equal(Math.round(res.body.tokens), 0); | ||
t.end(); | ||
}); | ||
request(app).get("/").end(response(t, 200, 0)) | ||
}); | ||
@@ -254,2 +198,3 @@ | ||
var app = create_app({ | ||
"burst": 1, | ||
"rate": "1/s", | ||
@@ -261,8 +206,4 @@ "on_throttled": function(req, res, next, bucket) { | ||
request(app).get("/").end(function() {}); | ||
request(app).get("/").end(function(err, res) { | ||
t.equal(res.status, 503); | ||
t.equal(Math.round(res.body.tokens), 0); | ||
t.end(); | ||
}); | ||
request(app).get("/").end(noop); | ||
request(app).get("/").end(response(t, 503, 0)); | ||
}); |
@@ -11,6 +11,4 @@ Multiple rate limits: Achieved by multiple throttles | ||
Change cache size | ||
Why? | ||
* Fun | ||
* More flexible |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
607
226
29389
14