UNPKG

5.78 kBJavaScriptView Raw
1"use strict";
2const MemoryStore = require("./memory-store");
3
4function RateLimit(options) {
5 options = Object.assign(
6 {
7 windowMs: 60 * 1000, // milliseconds - how long to keep records of requests in memory
8 max: 5, // max number of recent connections during `window` milliseconds before sending a 429 response
9 message: "Too many requests, please try again later.",
10 statusCode: 429, // 429 status = Too Many Requests (RFC 6585)
11 headers: true, //Send custom rate limit header with limit and remaining
12 draft_polli_ratelimit_headers: false, //Support for the new RateLimit standardization headers
13 skipFailedRequests: false, // Do not count failed requests (status >= 400)
14 skipSuccessfulRequests: false, // Do not count successful requests (status < 400)
15 // allows to create custom keys (by default user IP is used)
16 keyGenerator: function (req /*, res*/) {
17 return req.ip;
18 },
19 skip: function (/*req, res*/) {
20 return false;
21 },
22 handler: function (req, res /*, next*/) {
23 res.status(options.statusCode).send(options.message);
24 },
25 onLimitReached: function (/*req, res, optionsUsed*/) {},
26 },
27 options
28 );
29
30 // store to use for persisting rate limit data
31 options.store = options.store || new MemoryStore(options.windowMs);
32
33 // ensure that the store has the incr method
34 if (
35 typeof options.store.incr !== "function" ||
36 typeof options.store.resetKey !== "function" ||
37 (options.skipFailedRequests &&
38 typeof options.store.decrement !== "function")
39 ) {
40 throw new Error("The store is not valid.");
41 }
42
43 ["global", "delayMs", "delayAfter"].forEach((key) => {
44 // note: this doesn't trigger if delayMs or delayAfter are set to 0, because that essentially disables them
45 if (options[key]) {
46 throw new Error(
47 `The ${key} option was removed from express-rate-limit v3.`
48 );
49 }
50 });
51
52 function rateLimit(req, res, next) {
53 Promise.resolve(options.skip(req, res))
54 .then((skip) => {
55 if (skip) {
56 return next();
57 }
58
59 const key = options.keyGenerator(req, res);
60
61 options.store.incr(key, function (err, current, resetTime) {
62 if (err) {
63 return next(err);
64 }
65
66 const maxResult =
67 typeof options.max === "function"
68 ? options.max(req, res)
69 : options.max;
70
71 Promise.resolve(maxResult)
72 .then((max) => {
73 req.rateLimit = {
74 limit: max,
75 current: current,
76 remaining: Math.max(max - current, 0),
77 resetTime: resetTime,
78 };
79
80 if (options.headers && !res.headersSent) {
81 res.setHeader("X-RateLimit-Limit", max);
82 res.setHeader("X-RateLimit-Remaining", req.rateLimit.remaining);
83 if (resetTime instanceof Date) {
84 // if we have a resetTime, also provide the current date to help avoid issues with incorrect clocks
85 res.setHeader("Date", new Date().toUTCString());
86 res.setHeader(
87 "X-RateLimit-Reset",
88 Math.ceil(resetTime.getTime() / 1000)
89 );
90 }
91 }
92 if (options.draft_polli_ratelimit_headers && !res.headersSent) {
93 res.setHeader("RateLimit-Limit", max);
94 res.setHeader("RateLimit-Remaining", req.rateLimit.remaining);
95 if (resetTime) {
96 const deltaSeconds = Math.ceil(
97 (resetTime.getTime() - Date.now()) / 1000
98 );
99 res.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
100 }
101 }
102
103 if (
104 options.skipFailedRequests ||
105 options.skipSuccessfulRequests
106 ) {
107 let decremented = false;
108 const decrementKey = () => {
109 if (!decremented) {
110 options.store.decrement(key);
111 decremented = true;
112 }
113 };
114
115 if (options.skipFailedRequests) {
116 res.on("finish", function () {
117 if (res.statusCode >= 400) {
118 decrementKey();
119 }
120 });
121
122 res.on("close", () => {
123 if (!res.finished) {
124 decrementKey();
125 }
126 });
127
128 res.on("error", () => decrementKey());
129 }
130
131 if (options.skipSuccessfulRequests) {
132 res.on("finish", function () {
133 if (res.statusCode < 400) {
134 options.store.decrement(key);
135 }
136 });
137 }
138 }
139
140 if (max && current === max + 1) {
141 options.onLimitReached(req, res, options);
142 }
143
144 if (max && current > max) {
145 if (options.headers && !res.headersSent) {
146 res.setHeader(
147 "Retry-After",
148 Math.ceil(options.windowMs / 1000)
149 );
150 }
151 return options.handler(req, res, next);
152 }
153
154 next();
155
156 return null;
157 })
158 .catch(next);
159 });
160
161 return null;
162 })
163 .catch(next);
164 }
165
166 rateLimit.resetKey = options.store.resetKey.bind(options.store);
167
168 // Backward compatibility function
169 rateLimit.resetIp = rateLimit.resetKey;
170
171 return rateLimit;
172}
173
174module.exports = RateLimit;