Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

express-throttle

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

express-throttle - npm Package Compare versions

Comparing version 1.4.0 to 2.0.0

4

lib/memory-store.js

@@ -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
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc