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 0.1.0 to 1.0.0

106

lib/throttle.js
"use strict";
// The throttling is done using the "token bucket" method
// https://en.wikipedia.org/wiki/Token_bucket
// Default token storage (memory-bounded LRU cache)
var MemoryStore = require("./memory-store");
function Throttle() {
var options = parse_args(arguments);
function Throttle(options) {
var opts = parse_options(options);
var rate = opts.rate.amount / opts.rate.interval;
var burst = opts.burst || opts.rate.amount;
var store = opts.store || new MemoryStore(10000);
// Memory-bounded LRU cache
var store = options.store || new MemoryStore(10000);
// key function, used to identify the client we are going to throttle
var key = options.key || function(req) {
var key_func = opts.key || function(req) {
return req.headers["x-forwarded-for"] || req.connection.remoteAddress;
};
var on_throttled = options.on_throttled || function(req, res) {
// cost function, calculates the number of tokens to be subtracted per request
var cost_func;
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_throttled = opts.on_throttled || function(req, res) {
res.status(429).end();
};
return function(req, res, next) {
var k = key(req);
var key = key_func(req);
var cost = cost_func(req);
store.get(k, function(err, entry) {
store.get(key, function(err, entry) {
if (err) {

@@ -30,5 +43,5 @@ return next(err);

entry = entry || { "tokens": options.burst };
var is_conforming = consume_tokens(entry, options.burst, options.rate);
store.set(k, entry, function(err) {
entry = entry || { "tokens": burst };
var passthrough = consume_tokens(entry, rate, burst, cost);
store.set(key, entry, function(err) {
if (err) {

@@ -38,3 +51,3 @@ return next(err);

if (is_conforming) {
if (passthrough) {
next();

@@ -49,24 +62,44 @@ } else {

function parse_args(args) {
args = [].slice.call(args); // Convert to array
var options;
function shallow_clone(obj) {
var clone = {};
if (args.length === 1) {
options = args[0];
} else if (args.length === 2) {
options = { "burst": args[0], "rate": args[1] };
} else {
throw new Error("invalid number of arguments.");
for (var key in obj) {
clone[key] = obj[key];
}
if (typeof(options) != "object")
return clone;
}
function parse_options(options) {
if (typeof(options) == "string") {
options = { "rate": options };
}
if (typeof(options) != "object") {
throw new Error("options needs to be an object.");
} else {
options = shallow_clone(options);
}
if (typeof(options.rate) != "string") {
throw new Error("'rate' needs to be a string (e.g 2.5/s, 5/min, 10/day).");
}
if (typeof(options.burst) != "number")
options.rate = parse_rate(options.rate);
if (options.burst && typeof(options.burst) != "number") {
throw new Error("'burst' needs to be a number.");
}
if (options.key && typeof(options.key) != "function") {
throw new Error("'key' needs to be a function.");
}
if (typeof(options.rate) != "string")
throw new Error("'rate' needs to be a string of the form <integer>/<time-unit> (e.g 5/s, 10/min, 500/day)");
if (options.cost && !(typeof(options.cost) == "number" || typeof(options.cost) == "function")) {
throw new Error("'cost' needs to be a number or function.");
}
options.rate = parse_rate(options.rate);
if (options.on_throttled && typeof(options.on_throttled) != "function") {
throw new Error("'on_throttled' needs to be a function.");
}

@@ -76,3 +109,3 @@ return options;

var RATE_PATTERN = /^(\d+)\/(s|sec|second|m|min|minute|h|hour|d|day)$/;
var RATE_PATTERN = /^(\d+(\.\d+)?)\/(s|sec|second|m|min|minute|h|hour|d|day)$/;

@@ -82,9 +115,10 @@ function parse_rate(rate) {

if (!parsed_rate)
throw new Error("invalid rate, needs to be of the form <integer>/<time-unit> (e.g 5/s, 10/min, 500/day)");
if (!parsed_rate) {
throw new Error("invalid rate (e.g 2.5/s, 5/min, 10/day).");
}
var amount = parseInt(parsed_rate[1], 10);
var time_unit_in_ms = time_unit_to_ms(parsed_rate[2]);
var amount = parseFloat(parsed_rate[1]);
var interval = time_unit_to_ms(parsed_rate[3]);
return amount / time_unit_in_ms;
return { "amount": amount, "interval": interval };
}

@@ -107,3 +141,3 @@

function consume_tokens(entry, burst, rate) {
function consume_tokens(entry, rate, burst, cost) {
var now = Date.now();

@@ -120,3 +154,3 @@

if (entry.tokens >= 1) {
entry.tokens -= 1;
entry.tokens -= cost;
return true;

@@ -123,0 +157,0 @@ } else {

{
"name": "express-throttle",
"version": "0.1.0",
"version": "1.0.0",
"description": "Request throttling middleware for Express",

@@ -5,0 +5,0 @@ "main": "index.js",

# express-throttle
Request throttling middleware for Express framework
## Installation
```bash
$ npm install express-throttle
```
## Implementation
The throttling is done using the canonical [token bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm, where tokens are "refilled" in a sliding window manner (as opposed to a fixed time interval). This means that if we set maximum rate of 10 requests / minute, a client will not be able to send 10 requests 0:59, and 10 more 1:01. However, if the client sends 10 requests at 0:30, he will be able to send a new request at 0:36 (as tokens are regenerated continuously 1 every 6 seconds).
## Examples
```js
var express = require("express");
var throttle = require("express-throttle");
var app = express();
// Throttle to 5 reqs/s
app.post("/search", throttle("5/s"), 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).
```js
app.post("/search", throttle({ "rate": "5/s", "burst": 10 }), function(req, res, next) {
// ...
});
// Decimal values for rate values are allowed
app.post("/search", throttle({ "rate": "0.5/s", "burst": 5 }), function(req, res, next) {
// ...
});
```
By default, throttling is done on a per ip-address basis (respecting the X-Forwarded-For header). This can be configured by providing a custom key-function:
```js
var options = {
"rate": "5/s",
"burst": 10,
"key": function(req) {
return req.session.username;
}
};
app.post("/search", throttle(options), function(req, res, next) {
// ...
});
```
The "cost" per request can also be customized, making it possible to, for example whitelist certain requests:
```js
var whitelist = ["ip-1", "ip-2", ...];
var options = {
"rate": "5/s",
"burst": 10,
"cost": function(req) {
var ip_address = req.connection.remoteAddress;
if (whitelist.indexOf(ip_address) >= 0) {
return 0;
} else if (req.session.is_privileged_user) {
return 0.5;
} else {
return 1;
}
}
};
app.post("/search", throttle(options), function(req, res, next) {
// ...
});
var options = {
"rate": "1/s",
"burst": 10,
"cost": 2.5 // fixed costs are also supported
};
app.post("/expensive", throttle(options), function(req, res, next) {
// ...
});
```
Throttled requests will simply be responded with an empty 429 response. This can be overridden:
```js
var options = {
"rate": "5/s",
"burst": 10,
"on_throttled": function(req, res) {
// Possible course of actions:
// 1) Add client ip address to a ban list
// 2) Send back more information
res.set("X-Rate-Limit-Limit", 5);
res.status(503).send("System overloaded, try again at a later time.");
}
};
```
Throttling can be applied across multiple processes. This requires an external storage mechanism which can be configured as follows:
```js
function ExternalStorage(connection_settings) {
// ...
}
// These methods must be implemented
ExternalStorage.prototype.get = function(key, callback) {
fetch(key, function(entry) {
// First argument should be null if no errors occurred
callback(null, entry);
});
}
ExternalStorage.prototype.set = function(key, value, callback) {
save(key, value, function(err) {
// err should be null if no errors occurred
callback(err);
});
}
var options = {
"rate": "5/s",
"store": new ExternalStorage()
}
app.post("/search", throttle(options), function(req, res, next) {
// ...
});
```
## Options
`rate`: Determines the number of requests allowed within the specified time unit before subsequent requests get throttled. Must be specified according to the following format: *decimal/time-unit*
*decimal*: A non-negative decimal value. This value is internally stored as a float, but scientific notation is not supported (e.g 10e2).
*time-unit*: Any of the following: `s, sec, second, m, min, minute, h, hour, d, day`
`burst`: The number of requests that can be made at any rate. The burst quota is refilled with the specified `rate`.
`store`: Custom storage class. Must implement a `get` and `set` method with the following signatures:
```js
// callback in both methods must be called with an error (if any) as first argument
function get(key, callback) {
fetch(key, function(err, value) {
if (err) callback(err);
else callback(null, value);
});
}
function set(key, value, callback) {
// value will be an object with the following structure:
// { "tokens": Number, "accessed": Number }
save(key, value, function(err) {
callback(err);
}
}
```
Defaults to an LRU cache with a maximum of 10000 entries.
`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:
```js
function(req) {
return req.headers["x-forwarded-for"] || req.connection.remoteAddress;
}
```
`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.
`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:
```js
function(req, res) {
res.status(429).end();
};
```

@@ -10,2 +10,16 @@ "use strict";

function close_to(value, target, delta = 0.001) {
return Math.abs(value - target) < delta;
}
function create_app() {
var app = express();
app.get("/", throttle.apply(null, arguments), function(req, res) {
res.status(200).json(req.connection.remoteAddress);
});
return app;
}
test("fail to init...", t => {

@@ -17,67 +31,111 @@ t.test("...without options", st => {

t.test("...with options not being an object", st => {
st.throws(() => throttle("options"), new Error);
t.test("...with first argument not being a string or object", st => {
st.throws(() => throttle(5), new Error);
st.end();
});
t.test("...with 'burst' option not being a number", st => {
st.throws(() => throttle("5"), new Error);
t.test("...with empty option object", st => {
st.throws(() => throttle({}), new Error);
st.end();
});
t.test("...with 'rate' option not being a string", st => {
st.throws(() => throttle(5, 10), new Error);
t.test("...with 'key' not being a function", st => {
st.throws(() => throttle({ "rate": "1/s", "burst": 5, "key": 1 }), new Error);
st.end();
});
t.test("...without providing 'burst' option", st => {
st.throws(() => throttle({ "rate": "1/s" }), new Error);
t.test("...with 'cost' not being a number or function", st => {
st.throws(() => throttle({ "rate": "1/s", "burst": 5, "cost": "5" }), new Error);
st.end();
});
t.test("...without providing 'rate' option", st => {
st.throws(() => throttle({ "burst": 5 }), new Error);
t.test("...with 'on_throttled' not being a function", st => {
st.throws(() => throttle({ "rate": "1/s", "burst": 5, "on_throttled": "test" }), new Error);
st.end();
});
});
t.test("...with 'rate' not being in correct format", st => {
st.throws(() => throttle(5, "x/hour"), new Error);
test("init with...", t => {
t.test("...rate", st => {
st.doesNotThrow(() => throttle("1/s"));
st.end();
});
t.test("...options object", st => {
st.doesNotThrow(() => throttle({
"rate": "1/s",
"burst": 5,
"key": () => true,
"cost": () => true,
"on_throttled": () => true
}));
st.end();
});
});
test("successfully init with 'burst' and 'rate'", t => {
t.doesNotThrow(() => throttle(5, "1/s"));
t.end();
test("passthrough request...", t => {
function verify(st) {
return function(err, res) {
st.equal(res.status, 200);
st.end();
};
}
t.test("...rate (integer)", st => {
var app = create_app("1/s");
request(app).get("/").end(verify(st));
});
t.test("...rate (decimal)", st => {
var app = create_app("1.0/s");
request(app).get("/").end(verify(st));
});
t.test("...delayed", st => {
var app = create_app("1/s");
request(app).get("/").end(() => true);
setTimeout(() => {
request(app).get("/").end(verify(st));
}, 1000);
});
});
test("make one request", t => {
var app = express();
test("throttle request...", t => {
function verify(st) {
return function(err, res) {
st.equal(res.status, 429);
st.end();
};
}
app.get("/", throttle(5, "1/s"), function(req, res) {
res.status(200).end();
t.test("...rate (integer)", st => {
var app = create_app("1/s");
request(app).get("/").end(() => true);
request(app).get("/").end(verify(st));
});
request(app).get("/").end((err, res) => {
t.equal(res.status, 200);
t.end();
t.test("...rate (decimal)", st => {
var app = create_app("1.0/s");
request(app).get("/").end(() => true);
request(app).get("/").end(verify(st));
});
t.test("...delayed", st => {
var app = create_app("1/s");
request(app).get("/").end(() => true);
setTimeout(() => {
request(app).get("/").end(verify(st));
}, 900);
});
});
test("custom store + make one request", t => {
var app = express();
test("custom store", t => {
var store = new MemoryStore();
var app = create_app({ "rate": "1/s", "store": store });
app.get("/", throttle({
"burst": 5,
"rate": "1/s",
"store": store
}), function(req, res) {
res.status(200).json(req.connection.remoteAddress);
});
request(app).get("/").end((err, res) => {
t.equal(res.status, 200);
store.get(res.body, (err, entry) => {
t.equal(entry.tokens, 4);
t.ok(entry);
t.end();

@@ -89,21 +147,10 @@ });

test("respect x-forwarded-for header", t => {
var app = express();
var store = new MemoryStore();
app.get("/", throttle({
"burst": 5,
"rate": "1/s",
"store": store
}), function(req, res) {
res.status(200).json(req.connection.remoteAddress);
});
var proxy_ip = "123.123.123.123";
var app = create_app({ "rate": "1/s", "store": store });
request(app).get("/")
.set("x-forwarded-for", proxy_ip)
.end((err, res) => {
request(app).get("/").set("x-forwarded-for", proxy_ip).end((err, res) => {
t.equal(res.status, 200);
store.get(proxy_ip, (err, entry) => {
t.equal(entry.tokens, 4);
t.ok(entry);
t.end();

@@ -115,15 +162,8 @@ });

test("custom key function", t => {
var app = express();
var store = new MemoryStore();
var custom_key = "custom_key";
app.get("/", throttle({
"burst": 5,
var app = create_app({
"rate": "1/s",
"store": store,
"key": function() {
return custom_key;
}
}), function(req, res) {
res.status(200).end();
"key": function() { return custom_key; }
});

@@ -134,3 +174,3 @@

store.get(custom_key, (err, entry) => {
t.equal(entry.tokens, 4);
t.ok(entry);
t.end();

@@ -141,36 +181,66 @@ });

test("throttling", t => {
var app = express();
app.get("/", throttle(5, "1/s"), function(req, res) {
res.status(200).end();
test("custom cost value", t => {
var store = new MemoryStore();
var app = create_app({
"rate": "1/s",
"burst": 5,
"store": store,
"cost": 3
});
for (var i = 0; i < 5; ++i) {
request(app).get("/").end(() => true);
}
request(app).get("/").end((err, res) => {
t.equal(res.status, 429);
t.end();
store.get(res.body, (err, entry) => {
t.equal(res.status, 200);
t.assert(close_to(entry.tokens, 2));
t.end();
});
});
});
test("custom on_throttled function", t => {
test("custom cost function", t => {
var app = express();
app.get("/", throttle({
var store = new MemoryStore();
app.get("/:admin", throttle({
"burst": 5,
"rate": "1/s",
"on_throttled": function(req, res) {
res.status(429).json("slow down!");
"store": store,
"cost": function(req) {
if (req.params.admin == "yes") {
return 0;
} else {
return 3;
}
}
}), function(req, res) {
res.status(200).end();
res.status(200).json(req.connection.remoteAddress);
});
for (var i = 0; i < 5; ++i) {
request(app).get("/").end(() => true);
}
request(app).get("/yes").end((err, res) => {
store.get(res.body, (err, entry) => {
t.equal(res.status, 200);
t.assert(close_to(entry.tokens, 5));
request(app).get("/no").end((err, res) => {
store.get(res.body, (err, entry) => {
t.equal(res.status, 200);
t.assert(close_to(entry.tokens, 2));
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!");
}
});
request(app).get("/").end(() => true);
request(app).get("/").end((err, res) => {
t.equal(res.status, 429);
t.equal(res.status, 503);
t.equal(res.body, "slow down!");

@@ -177,0 +247,0 @@ t.end();

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