express-rate-limit
Advanced tools
Comparing version
@@ -13,6 +13,15 @@ "use strict"; | ||
draft_polli_ratelimit_headers: false, //Support for the new RateLimit standardization headers | ||
skipFailedRequests: false, // Do not count failed requests (status >= 400) | ||
skipSuccessfulRequests: false, // Do not count successful requests (status < 400) | ||
// ability to manually decide if request was successful. Used when `skipSuccessfulRequests` and/or `skipFailedRequests` are set to `true` | ||
requestWasSuccessful: function (req, res) { | ||
return res.statusCode < 400; | ||
}, | ||
skipFailedRequests: false, // Do not count failed requests | ||
skipSuccessfulRequests: false, // Do not count successful requests | ||
// allows to create custom keys (by default user IP is used) | ||
keyGenerator: function (req /*, res*/) { | ||
if (!req.ip) { | ||
console.error( | ||
"express-rate-limit: req.ip is undefined - you can avoid this by providing a custom keyGenerator function, but it may be indicative of a larger issue." | ||
); | ||
} | ||
return req.ip; | ||
@@ -23,6 +32,7 @@ }, | ||
}, | ||
handler: function (req, res /*, next*/) { | ||
handler: function (req, res /*, next, optionsUsed*/) { | ||
res.status(options.statusCode).send(options.message); | ||
}, | ||
onLimitReached: function (/*req, res, optionsUsed*/) {}, | ||
requestPropertyName: "rateLimit", // Parameter name appended to req object | ||
}, | ||
@@ -55,97 +65,119 @@ options | ||
function rateLimit(req, res, next) { | ||
if (options.skip(req, res)) { | ||
return next(); | ||
} | ||
Promise.resolve(options.skip(req, res)) | ||
.then((skip) => { | ||
if (skip) { | ||
return next(); | ||
} | ||
const key = options.keyGenerator(req, res); | ||
const key = options.keyGenerator(req, res); | ||
options.store.incr(key, function (err, current, resetTime) { | ||
if (err) { | ||
return next(err); | ||
} | ||
options.store.incr(key, function (err, current, resetTime) { | ||
if (err) { | ||
return next(err); | ||
} | ||
const maxResult = | ||
typeof options.max === "function" ? options.max(req, res) : options.max; | ||
const maxResult = | ||
typeof options.max === "function" | ||
? options.max(req, res) | ||
: options.max; | ||
Promise.resolve(maxResult) | ||
.then((max) => { | ||
req.rateLimit = { | ||
limit: max, | ||
current: current, | ||
remaining: Math.max(max - current, 0), | ||
resetTime: resetTime, | ||
}; | ||
Promise.resolve(maxResult) | ||
.then((max) => { | ||
req[options.requestPropertyName] = { | ||
limit: max, | ||
current: current, | ||
remaining: Math.max(max - current, 0), | ||
resetTime: resetTime, | ||
}; | ||
if (options.headers && !res.headersSent) { | ||
res.setHeader("X-RateLimit-Limit", max); | ||
res.setHeader("X-RateLimit-Remaining", req.rateLimit.remaining); | ||
if (resetTime instanceof Date) { | ||
// if we have a resetTime, also provide the current date to help avoid issues with incorrect clocks | ||
res.setHeader("Date", new Date().toGMTString()); | ||
res.setHeader( | ||
"X-RateLimit-Reset", | ||
Math.ceil(resetTime.getTime() / 1000) | ||
); | ||
} | ||
} | ||
if (options.draft_polli_ratelimit_headers && !res.headersSent) { | ||
res.setHeader("RateLimit-Limit", max); | ||
res.setHeader("RateLimit-Remaining", req.rateLimit.remaining); | ||
if (resetTime) { | ||
const deltaSeconds = Math.ceil( | ||
(resetTime.getTime() - Date.now()) / 1000 | ||
); | ||
res.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds)); | ||
} | ||
} | ||
if (options.skipFailedRequests || options.skipSuccessfulRequests) { | ||
let decremented = false; | ||
const decrementKey = () => { | ||
if (!decremented) { | ||
options.store.decrement(key); | ||
decremented = true; | ||
if (options.headers && !res.headersSent) { | ||
res.setHeader("X-RateLimit-Limit", max); | ||
res.setHeader( | ||
"X-RateLimit-Remaining", | ||
req[options.requestPropertyName].remaining | ||
); | ||
if (resetTime instanceof Date) { | ||
// if we have a resetTime, also provide the current date to help avoid issues with incorrect clocks | ||
res.setHeader("Date", new Date().toUTCString()); | ||
res.setHeader( | ||
"X-RateLimit-Reset", | ||
Math.ceil(resetTime.getTime() / 1000) | ||
); | ||
} | ||
} | ||
}; | ||
if (options.draft_polli_ratelimit_headers && !res.headersSent) { | ||
res.setHeader("RateLimit-Limit", max); | ||
res.setHeader( | ||
"RateLimit-Remaining", | ||
req[options.requestPropertyName].remaining | ||
); | ||
if (resetTime) { | ||
const deltaSeconds = Math.ceil( | ||
(resetTime.getTime() - Date.now()) / 1000 | ||
); | ||
res.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds)); | ||
} | ||
} | ||
if (options.skipFailedRequests) { | ||
res.on("finish", function () { | ||
if (res.statusCode >= 400) { | ||
decrementKey(); | ||
if ( | ||
options.skipFailedRequests || | ||
options.skipSuccessfulRequests | ||
) { | ||
let decremented = false; | ||
const decrementKey = () => { | ||
if (!decremented) { | ||
options.store.decrement(key); | ||
decremented = true; | ||
} | ||
}; | ||
if (options.skipFailedRequests) { | ||
res.on("finish", function () { | ||
if (!options.requestWasSuccessful(req, res)) { | ||
decrementKey(); | ||
} | ||
}); | ||
res.on("close", () => { | ||
if (!res.finished) { | ||
decrementKey(); | ||
} | ||
}); | ||
res.on("error", () => decrementKey()); | ||
} | ||
}); | ||
res.on("close", () => { | ||
if (!res.finished) { | ||
decrementKey(); | ||
if (options.skipSuccessfulRequests) { | ||
res.on("finish", function () { | ||
if (options.requestWasSuccessful(req, res)) { | ||
options.store.decrement(key); | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
res.on("error", () => decrementKey()); | ||
} | ||
if (max && current === max + 1) { | ||
options.onLimitReached(req, res, options); | ||
} | ||
if (options.skipSuccessfulRequests) { | ||
res.on("finish", function () { | ||
if (res.statusCode < 400) { | ||
options.store.decrement(key); | ||
if (max && current > max) { | ||
if (options.headers && !res.headersSent) { | ||
res.setHeader( | ||
"Retry-After", | ||
Math.ceil(options.windowMs / 1000) | ||
); | ||
} | ||
}); | ||
} | ||
} | ||
return options.handler(req, res, next, options); | ||
} | ||
if (max && current === max + 1) { | ||
options.onLimitReached(req, res, options); | ||
} | ||
next(); | ||
if (max && current > max) { | ||
if (options.headers && !res.headersSent) { | ||
res.setHeader("Retry-After", Math.ceil(options.windowMs / 1000)); | ||
} | ||
return options.handler(req, res, next); | ||
} | ||
return null; | ||
}) | ||
.catch(next); | ||
}); | ||
next(); | ||
}) | ||
.catch(next); | ||
}); | ||
return null; | ||
}) | ||
.catch(next); | ||
} | ||
@@ -152,0 +184,0 @@ |
{ | ||
"name": "express-rate-limit", | ||
"version": "5.1.3", | ||
"version": "5.5.1", | ||
"description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.", | ||
@@ -34,13 +34,14 @@ "homepage": "https://github.com/nfriedly/express-rate-limit", | ||
], | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"eslint": "^6.8.0", | ||
"eslint-config-prettier": "^6.10.1", | ||
"eslint-plugin-prettier": "^3.1.2", | ||
"bluebird": "^3.7.2", | ||
"eslint": "^7.32.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"express": "^4.17.1", | ||
"husky": "^4.2.3", | ||
"mocha": "^7.1.1", | ||
"prettier": "^2.0.4", | ||
"pretty-quick": "^2.0.1", | ||
"supertest": "^4.0.2" | ||
"husky": "^7.0.2", | ||
"mocha": "^9.1.2", | ||
"prettier": "^2.4.1", | ||
"pretty-quick": "^3.1.1", | ||
"sinon": "^11.1.2", | ||
"supertest": "^6.1.6" | ||
}, | ||
@@ -47,0 +48,0 @@ "scripts": { |
# Express Rate Limit | ||
[](http://travis-ci.org/nfriedly/express-rate-limit) | ||
[](https://npmjs.org/package/express-rate-limit "View this project on NPM") | ||
[](https://david-dm.org/nfriedly/express-rate-limit) | ||
[](https://david-dm.org/nfriedly/express-rate-limit#info=devDependencies) | ||
[](https://github.com/nfriedly/express-rate-limit/actions) | ||
[](https://npmjs.org/package/express-rate-limit "View this project on NPM") | ||
[](https://www.npmjs.com/package/express-rate-limit) | ||
Basic rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | ||
@@ -12,11 +12,11 @@ | ||
Note: this module does not share state with other processes/servers by default. | ||
If you need a more robust solution, I recommend using an external store: | ||
Note: this module does not share state with other processes/servers by default. It also buckets all requests to an internal clock rather than starting a new timer for each end-user. It's fine for abuse-prevention but might not produce the desired effect when attempting to strictly enforce API rate-limits or similar. If you need a more robust solution, I recommend using an external store: | ||
### Stores | ||
- Memory Store _(default, built-in)_ - stores hits in-memory in the Node.js process. Does not share state with other servers or processes. | ||
- Memory Store _(default, built-in)_ - stores hits in-memory in the Node.js process. Does not share state with other servers or processes, and does not start a separate timer for each end user. | ||
- [Redis Store](https://npmjs.com/package/rate-limit-redis) | ||
- [Memcached Store](https://npmjs.org/package/rate-limit-memcached) | ||
- [Mongo Store](https://www.npmjs.com/package/rate-limit-mongo) | ||
- [Precise Memory Store](https://www.npmjs.com/package/precise-memory-rate-limit) - similar to the built-in memory store except that it stores a distinct timestamp for each IP rather than bucketing them together. | ||
@@ -44,3 +44,3 @@ ### Alternate Rate-limiters | ||
// Enable if you're behind a reverse proxy (Heroku, Bluemix, AWS ELB, Nginx, etc) | ||
// Enable if you're behind a reverse proxy (Heroku, Bluemix, AWS ELB or API Gateway, Nginx, etc) | ||
// see https://expressjs.com/en/guide/behind-proxies.html | ||
@@ -63,3 +63,3 @@ // app.set('trust proxy', 1); | ||
// Enable if you're behind a reverse proxy (Heroku, Bluemix, AWS ELB, Nginx, etc) | ||
// Enable if you're behind a reverse proxy (Heroku, Bluemix, AWS ELB or API Gateway, Nginx, etc) | ||
// see https://expressjs.com/en/guide/behind-proxies.html | ||
@@ -109,2 +109,4 @@ // app.set('trust proxy', 1); | ||
The property name can be configured with the configuration option `requestPropertyName` | ||
## Configuration options | ||
@@ -120,2 +122,27 @@ | ||
Example of using a function: | ||
```js | ||
const rateLimit = require("express-rate-limit"); | ||
function isPremium(req) { | ||
//... | ||
} | ||
const limiter = rateLimit({ | ||
windowMs: 15 * 60 * 1000, // 15 minutes | ||
// max could also be an async function or return a promise | ||
max: function(req, res) { | ||
if (isPremium(req)) { | ||
return 10; | ||
} | ||
return 5; | ||
} | ||
}); | ||
// apply to all requests | ||
app.use(limiter); | ||
``` | ||
### windowMs | ||
@@ -159,3 +186,3 @@ | ||
Defaults to req.ip: | ||
Defaults to req.ip, similar to this: | ||
@@ -170,3 +197,3 @@ ```js | ||
The function to handle requests once the max limit is exceeded. It receives the request and the response objects. The "next" param is available if you need to pass to the next middleware. | ||
The function to handle requests once the max limit is exceeded. It receives the request and the response objects. The "next" param is available if you need to pass to the next middleware. Finally, the options param has all of the options that originally passed in when creating the current limiter and the default values for other options. | ||
@@ -178,3 +205,3 @@ The`req.rateLimit` object has `limit`, `current`, and `remaining` number of requests and, if the store provides it, a `resetTime` Date object. | ||
```js | ||
function (req, res, /*next*/) { | ||
function (req, res, next, options) { | ||
res.status(options.statusCode).send(options.message); | ||
@@ -198,2 +225,15 @@ } | ||
### requestWasSuccessful | ||
Function that is called when `skipFailedRequests` and/or `skipSuccessfulRequests` are set to `true`. | ||
May be overridden if, for example, a service sends out a 200 status code on errors. | ||
Defaults to | ||
```js | ||
function (req, res) { | ||
return res.statusCode < 400; | ||
} | ||
``` | ||
### skipFailedRequests | ||
@@ -220,3 +260,3 @@ | ||
Function used to skip (whitelist) requests. Returning `true` from the function will skip limiting for that request. | ||
Function used to skip (whitelist) requests. Returning `true`, or a promise that resolves with `true`, from the function will skip limiting for that request. | ||
@@ -231,2 +271,7 @@ Defaults to always `false` (count all requests): | ||
### requestPropertyName | ||
Parameter to add to `req`-Object. | ||
Defaults to `rateLimit`. | ||
### store | ||
@@ -233,0 +278,0 @@ |
Sorry, the diff of this file is not supported yet
22105
14.48%204
17.24%351
14.71%11
22.22%