express-rate-limit 6.8.1 → 6.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +28 -0
- package/dist/index.cjs +228 -36
- package/dist/index.d.cts +15 -1
- package/dist/index.d.mts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.mjs +228 -36
- package/package.json +16 -40
- package/readme.md +91 -35
- package/tsconfig.json +1 -9
package/changelog.md
CHANGED
|
@@ -6,6 +6,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to
|
|
7
7
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [6.10.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.10.0)
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Support for combined `RateLimit` header from the
|
|
14
|
+
[RateLimit header fields for HTTP standardization draft](https://github.com/ietf-wg-httpapi/ratelimit-headers)
|
|
15
|
+
adopted by the IETF. Enable by setting `standardHeaders: 'draft-7'`
|
|
16
|
+
- New `standardHeaders: 'draft-6'` option, treated equivalent to
|
|
17
|
+
`standardHeaders: true` from previous releases. (`true` and `false` are still
|
|
18
|
+
supported.)
|
|
19
|
+
- New `RateLimit-Policy` header added when `standardHeaders` is set to
|
|
20
|
+
`'draft-6'`, `'draft-7'`, or `true`
|
|
21
|
+
- Warning when using deprecated `draft_polli_ratelimit_headers` option
|
|
22
|
+
- Warning when using deprecated `onLimitReached` option
|
|
23
|
+
- Warning when `totalHits` value returned from Store is invalid
|
|
24
|
+
|
|
25
|
+
## [6.9.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.9.0)
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- New validaion check for double-counted requests
|
|
30
|
+
- Added help link to each ValidationError, directing users to the appropriate
|
|
31
|
+
wiki page for more info
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Miscaleanous documenation improvements
|
|
36
|
+
|
|
9
37
|
## [6.8.1](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.8.0) & [6.7.2](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.8.0)
|
|
10
38
|
|
|
11
39
|
### Changed
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,59 @@ __export(source_exports, {
|
|
|
31
31
|
});
|
|
32
32
|
module.exports = __toCommonJS(source_exports);
|
|
33
33
|
|
|
34
|
+
// source/headers.ts
|
|
35
|
+
var getResetSeconds = (resetTime, windowMs) => {
|
|
36
|
+
let resetSeconds = void 0;
|
|
37
|
+
if (resetTime) {
|
|
38
|
+
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
|
39
|
+
resetSeconds = Math.max(0, deltaSeconds);
|
|
40
|
+
} else if (windowMs) {
|
|
41
|
+
resetSeconds = Math.ceil(windowMs / 1e3);
|
|
42
|
+
}
|
|
43
|
+
return resetSeconds;
|
|
44
|
+
};
|
|
45
|
+
var setLegacyHeaders = (response, info) => {
|
|
46
|
+
if (response.headersSent)
|
|
47
|
+
return;
|
|
48
|
+
response.setHeader("X-RateLimit-Limit", info.limit);
|
|
49
|
+
response.setHeader("X-RateLimit-Remaining", info.remaining);
|
|
50
|
+
if (info.resetTime instanceof Date) {
|
|
51
|
+
response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
|
|
52
|
+
response.setHeader(
|
|
53
|
+
"X-RateLimit-Reset",
|
|
54
|
+
Math.ceil(info.resetTime.getTime() / 1e3)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var setDraft6Headers = (response, info, windowMs) => {
|
|
59
|
+
if (response.headersSent)
|
|
60
|
+
return;
|
|
61
|
+
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
62
|
+
const resetSeconds = getResetSeconds(info.resetTime);
|
|
63
|
+
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
64
|
+
response.setHeader("RateLimit-Limit", info.limit);
|
|
65
|
+
response.setHeader("RateLimit-Remaining", info.remaining);
|
|
66
|
+
if (resetSeconds)
|
|
67
|
+
response.setHeader("RateLimit-Reset", resetSeconds);
|
|
68
|
+
};
|
|
69
|
+
var setDraft7Headers = (response, info, windowMs) => {
|
|
70
|
+
if (response.headersSent)
|
|
71
|
+
return;
|
|
72
|
+
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
73
|
+
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
74
|
+
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
75
|
+
response.setHeader(
|
|
76
|
+
"RateLimit",
|
|
77
|
+
`limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
var setRetryAfterHeader = (response, info, windowMs) => {
|
|
81
|
+
if (response.headersSent)
|
|
82
|
+
return;
|
|
83
|
+
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
84
|
+
response.setHeader("Retry-After", resetSeconds);
|
|
85
|
+
};
|
|
86
|
+
|
|
34
87
|
// source/validations.ts
|
|
35
88
|
var import_node_net = require("net");
|
|
36
89
|
var ValidationError = class extends Error {
|
|
@@ -38,21 +91,23 @@ var ValidationError = class extends Error {
|
|
|
38
91
|
* The code must be a string, in snake case and all capital, that starts with
|
|
39
92
|
* the substring `ERR_ERL_`.
|
|
40
93
|
*
|
|
41
|
-
* The message must be a string, starting with
|
|
94
|
+
* The message must be a string, starting with an uppercase character,
|
|
42
95
|
* describing the issue in detail.
|
|
43
96
|
*/
|
|
44
97
|
constructor(code, message) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
98
|
+
const url = `https://express-rate-limit.github.io/${code}/`;
|
|
99
|
+
super(`${message} See ${url} for more information.`);
|
|
48
100
|
__publicField(this, "name");
|
|
49
101
|
__publicField(this, "code");
|
|
102
|
+
__publicField(this, "help");
|
|
50
103
|
this.name = this.constructor.name;
|
|
51
104
|
this.code = code;
|
|
52
|
-
this.
|
|
105
|
+
this.help = url;
|
|
53
106
|
}
|
|
54
107
|
};
|
|
55
|
-
var
|
|
108
|
+
var ChangeWarning = class extends ValidationError {
|
|
109
|
+
};
|
|
110
|
+
var _Validations = class _Validations {
|
|
56
111
|
constructor(enabled) {
|
|
57
112
|
// eslint-disable-next-line @typescript-eslint/parameter-properties
|
|
58
113
|
__publicField(this, "enabled");
|
|
@@ -129,6 +184,125 @@ var Validations = class {
|
|
|
129
184
|
}
|
|
130
185
|
});
|
|
131
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Ensures totalHits value from store is a positive integer.
|
|
189
|
+
*
|
|
190
|
+
* @param hits {any} - The `totalHits` returned by the store.
|
|
191
|
+
*/
|
|
192
|
+
positiveHits(hits) {
|
|
193
|
+
this.wrap(() => {
|
|
194
|
+
if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
|
|
195
|
+
throw new ValidationError(
|
|
196
|
+
"ERR_ERL_INVALID_HITS",
|
|
197
|
+
`The totalHits value returned from the store must be a positive integer, got ${hits}`
|
|
198
|
+
// eslint-disable-line @typescript-eslint/restrict-template-expressions
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Ensures a given key is incremented only once per request.
|
|
205
|
+
*
|
|
206
|
+
* @param request {Request} - The Express request object.
|
|
207
|
+
* @param store {Store} - The store class.
|
|
208
|
+
* @param key {string} - The key used to store the client's hit count.
|
|
209
|
+
*
|
|
210
|
+
* @returns {void}
|
|
211
|
+
*/
|
|
212
|
+
singleCount(request, store, key) {
|
|
213
|
+
this.wrap(() => {
|
|
214
|
+
let storeKeys = _Validations.singleCountKeys.get(request);
|
|
215
|
+
if (!storeKeys) {
|
|
216
|
+
storeKeys = /* @__PURE__ */ new Map();
|
|
217
|
+
_Validations.singleCountKeys.set(request, storeKeys);
|
|
218
|
+
}
|
|
219
|
+
const storeKey = store.localKeys ? store : store.constructor.name;
|
|
220
|
+
let keys = storeKeys.get(storeKey);
|
|
221
|
+
if (!keys) {
|
|
222
|
+
keys = [];
|
|
223
|
+
storeKeys.set(storeKey, keys);
|
|
224
|
+
}
|
|
225
|
+
if (keys.includes(key)) {
|
|
226
|
+
throw new ValidationError(
|
|
227
|
+
"ERR_ERL_DOUBLE_COUNT",
|
|
228
|
+
`The hit count for ${key} was incremented more than once for a single request.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
keys.push(key);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Warns the user that the behaviour for `max: 0` is changing in the next
|
|
236
|
+
* major release.
|
|
237
|
+
*
|
|
238
|
+
* @param max {number} - The maximum number of hits per client.
|
|
239
|
+
*
|
|
240
|
+
* @returns {void}
|
|
241
|
+
*/
|
|
242
|
+
max(max) {
|
|
243
|
+
this.wrap(() => {
|
|
244
|
+
if (max === 0) {
|
|
245
|
+
throw new ChangeWarning(
|
|
246
|
+
"WRN_ERL_MAX_ZERO",
|
|
247
|
+
`Setting max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
|
|
254
|
+
* and will be removed in the next major release.
|
|
255
|
+
*
|
|
256
|
+
* @param draft_polli_ratelimit_headers {boolean|undefined} - The now-deprecated setting that was used to enable standard headers.
|
|
257
|
+
*
|
|
258
|
+
* @returns {void}
|
|
259
|
+
*/
|
|
260
|
+
draftPolliHeaders(draft_polli_ratelimit_headers) {
|
|
261
|
+
this.wrap(() => {
|
|
262
|
+
if (draft_polli_ratelimit_headers) {
|
|
263
|
+
throw new ChangeWarning(
|
|
264
|
+
"WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
|
|
265
|
+
`The draft_polli_ratelimit_headers configuration option is deprecated and will be removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Warns the user that the `onLimitReached` option is deprecated and will be removed in the next
|
|
272
|
+
* major release.
|
|
273
|
+
*
|
|
274
|
+
* @param onLimitReached {function|undefined} - The maximum number of hits per client.
|
|
275
|
+
*
|
|
276
|
+
* @returns {void}
|
|
277
|
+
*/
|
|
278
|
+
onLimitReached(onLimitReached) {
|
|
279
|
+
this.wrap(() => {
|
|
280
|
+
if (onLimitReached) {
|
|
281
|
+
throw new ChangeWarning(
|
|
282
|
+
"WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
|
|
283
|
+
`The onLimitReached configuration option is deprecated and will be removed in express-rate-limit v7.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Warns the user when the selected headers option requires a reset time but
|
|
290
|
+
* the store does not provide one.
|
|
291
|
+
*
|
|
292
|
+
* @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
|
|
293
|
+
*
|
|
294
|
+
* @returns {void}
|
|
295
|
+
*/
|
|
296
|
+
headersResetTime(resetTime) {
|
|
297
|
+
this.wrap(() => {
|
|
298
|
+
if (!resetTime) {
|
|
299
|
+
throw new ValidationError(
|
|
300
|
+
"ERR_ERL_HEADERS_NO_RESET",
|
|
301
|
+
`standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
132
306
|
wrap(validation) {
|
|
133
307
|
if (!this.enabled) {
|
|
134
308
|
return;
|
|
@@ -136,10 +310,25 @@ var Validations = class {
|
|
|
136
310
|
try {
|
|
137
311
|
validation.call(this);
|
|
138
312
|
} catch (error) {
|
|
139
|
-
|
|
313
|
+
if (error instanceof ChangeWarning)
|
|
314
|
+
console.warn(error);
|
|
315
|
+
else
|
|
316
|
+
console.error(error);
|
|
140
317
|
}
|
|
141
318
|
}
|
|
142
319
|
};
|
|
320
|
+
/**
|
|
321
|
+
* Maps the key used in a store for a certain request, and ensures that the
|
|
322
|
+
* same key isn't used more than once per request.
|
|
323
|
+
*
|
|
324
|
+
* The store can be any one of the following:
|
|
325
|
+
* - An instance, for stores like the MemoryStore where two instances do not
|
|
326
|
+
* share state.
|
|
327
|
+
* - A string (class name), for stores where multiple instances
|
|
328
|
+
* typically share state, such as the Redis store.
|
|
329
|
+
*/
|
|
330
|
+
__publicField(_Validations, "singleCountKeys", /* @__PURE__ */ new WeakMap());
|
|
331
|
+
var Validations = _Validations;
|
|
143
332
|
|
|
144
333
|
// source/memory-store.ts
|
|
145
334
|
var calculateNextResetTime = (windowMs) => {
|
|
@@ -165,6 +354,11 @@ var MemoryStore = class {
|
|
|
165
354
|
* Reference to the active timer.
|
|
166
355
|
*/
|
|
167
356
|
__publicField(this, "interval");
|
|
357
|
+
/**
|
|
358
|
+
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
359
|
+
* cannot affect other instances.
|
|
360
|
+
*/
|
|
361
|
+
__publicField(this, "localKeys", true);
|
|
168
362
|
}
|
|
169
363
|
/**
|
|
170
364
|
* Method that initializes the store.
|
|
@@ -271,6 +465,7 @@ var promisifyStore = (passedStore) => {
|
|
|
271
465
|
async resetKey(key) {
|
|
272
466
|
return legacyStore.resetKey(key);
|
|
273
467
|
}
|
|
468
|
+
/* istanbul ignore next */
|
|
274
469
|
async resetAll() {
|
|
275
470
|
if (typeof legacyStore.resetAll === "function")
|
|
276
471
|
return legacyStore.resetAll();
|
|
@@ -299,13 +494,20 @@ var parseOptions = (passedOptions) => {
|
|
|
299
494
|
var _a, _b, _c, _d;
|
|
300
495
|
const notUndefinedOptions = omitUndefinedOptions(passedOptions);
|
|
301
496
|
const validations = new Validations((_a = notUndefinedOptions == null ? void 0 : notUndefinedOptions.validate) != null ? _a : true);
|
|
497
|
+
validations.draftPolliHeaders(
|
|
498
|
+
notUndefinedOptions.draft_polli_ratelimit_headers
|
|
499
|
+
);
|
|
500
|
+
validations.onLimitReached(notUndefinedOptions.onLimitReached);
|
|
501
|
+
let standardHeaders = (_b = notUndefinedOptions.standardHeaders) != null ? _b : false;
|
|
502
|
+
if (standardHeaders === true || standardHeaders === void 0 && notUndefinedOptions.draft_polli_ratelimit_headers) {
|
|
503
|
+
standardHeaders = "draft-6";
|
|
504
|
+
}
|
|
302
505
|
const config = {
|
|
303
506
|
windowMs: 60 * 1e3,
|
|
304
507
|
max: 5,
|
|
305
508
|
message: "Too many requests, please try again later.",
|
|
306
509
|
statusCode: 429,
|
|
307
|
-
legacyHeaders: (
|
|
308
|
-
standardHeaders: (_c = passedOptions.draft_polli_ratelimit_headers) != null ? _c : false,
|
|
510
|
+
legacyHeaders: (_c = passedOptions.headers) != null ? _c : true,
|
|
309
511
|
requestPropertyName: "rateLimit",
|
|
310
512
|
skipFailedRequests: false,
|
|
311
513
|
skipSuccessfulRequests: false,
|
|
@@ -324,13 +526,15 @@ var parseOptions = (passedOptions) => {
|
|
|
324
526
|
response
|
|
325
527
|
) : config.message;
|
|
326
528
|
if (!response.writableEnded) {
|
|
327
|
-
response.send(message
|
|
529
|
+
response.send(message);
|
|
328
530
|
}
|
|
329
531
|
},
|
|
330
532
|
onLimitReached(_request, _response, _optionsUsed) {
|
|
331
533
|
},
|
|
332
|
-
// Allow the options
|
|
534
|
+
// Allow the default options to be overriden by the options passed to the middleware.
|
|
333
535
|
...notUndefinedOptions,
|
|
536
|
+
// `standardHeaders` is resolved into a draft version above, use that.
|
|
537
|
+
standardHeaders,
|
|
334
538
|
// Note that this field is declared after the user's options are spread in,
|
|
335
539
|
// so that this field doesn't get overriden with an un-promisified store!
|
|
336
540
|
store: promisifyStore((_d = notUndefinedOptions.store) != null ? _d : new MemoryStore()),
|
|
@@ -366,39 +570,27 @@ var rateLimit = (passedOptions) => {
|
|
|
366
570
|
const augmentedRequest = request;
|
|
367
571
|
const key = await config.keyGenerator(request, response);
|
|
368
572
|
const { totalHits, resetTime } = await config.store.increment(key);
|
|
573
|
+
config.validations.positiveHits(totalHits);
|
|
574
|
+
config.validations.singleCount(request, config.store, key);
|
|
369
575
|
const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max;
|
|
370
576
|
const maxHits = await retrieveQuota;
|
|
371
|
-
|
|
577
|
+
config.validations.max(maxHits);
|
|
578
|
+
const info = {
|
|
372
579
|
limit: maxHits,
|
|
373
580
|
current: totalHits,
|
|
374
581
|
remaining: Math.max(maxHits - totalHits, 0),
|
|
375
582
|
resetTime
|
|
376
583
|
};
|
|
584
|
+
augmentedRequest[config.requestPropertyName] = info;
|
|
377
585
|
if (config.legacyHeaders && !response.headersSent) {
|
|
378
|
-
response
|
|
379
|
-
response.setHeader(
|
|
380
|
-
"X-RateLimit-Remaining",
|
|
381
|
-
augmentedRequest[config.requestPropertyName].remaining
|
|
382
|
-
);
|
|
383
|
-
if (resetTime instanceof Date) {
|
|
384
|
-
response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
|
|
385
|
-
response.setHeader(
|
|
386
|
-
"X-RateLimit-Reset",
|
|
387
|
-
Math.ceil(resetTime.getTime() / 1e3)
|
|
388
|
-
);
|
|
389
|
-
}
|
|
586
|
+
setLegacyHeaders(response, info);
|
|
390
587
|
}
|
|
391
588
|
if (config.standardHeaders && !response.headersSent) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (resetTime) {
|
|
398
|
-
const deltaSeconds = Math.ceil(
|
|
399
|
-
(resetTime.getTime() - Date.now()) / 1e3
|
|
400
|
-
);
|
|
401
|
-
response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
|
|
589
|
+
if (config.standardHeaders === "draft-6") {
|
|
590
|
+
setDraft6Headers(response, info, config.windowMs);
|
|
591
|
+
} else if (config.standardHeaders === "draft-7") {
|
|
592
|
+
config.validations.headersResetTime(info.resetTime);
|
|
593
|
+
setDraft7Headers(response, info, config.windowMs);
|
|
402
594
|
}
|
|
403
595
|
}
|
|
404
596
|
if (config.skipFailedRequests || config.skipSuccessfulRequests) {
|
|
@@ -434,8 +626,8 @@ var rateLimit = (passedOptions) => {
|
|
|
434
626
|
}
|
|
435
627
|
config.validations.disable();
|
|
436
628
|
if (maxHits && totalHits > maxHits) {
|
|
437
|
-
if (
|
|
438
|
-
response
|
|
629
|
+
if (config.legacyHeaders || config.standardHeaders) {
|
|
630
|
+
setRetryAfterHeader(response, info, config.windowMs);
|
|
439
631
|
}
|
|
440
632
|
config.handler(request, response, next, options);
|
|
441
633
|
return;
|
package/dist/index.d.cts
CHANGED
|
@@ -130,7 +130,16 @@ export type Store = {
|
|
|
130
130
|
* Method to shutdown the store, stop timers, and release all resources.
|
|
131
131
|
*/
|
|
132
132
|
shutdown?: () => Promise<void> | void;
|
|
133
|
+
/**
|
|
134
|
+
* Flag to indicate that keys incremented in one instance of this store can
|
|
135
|
+
* not affect other instances. Typically false if a database is used, true for
|
|
136
|
+
* MemoryStore.
|
|
137
|
+
*
|
|
138
|
+
* Used to help detect double-counting misconfigurations.
|
|
139
|
+
*/
|
|
140
|
+
localKeys?: boolean;
|
|
133
141
|
};
|
|
142
|
+
export type DraftHeadersVersion = "draft-6" | "draft-7";
|
|
134
143
|
/**
|
|
135
144
|
* The configuration options for the rate limiter.
|
|
136
145
|
*/
|
|
@@ -175,7 +184,7 @@ export type Options = {
|
|
|
175
184
|
*
|
|
176
185
|
* Defaults to `false` (for backward compatibility, but its use is recommended).
|
|
177
186
|
*/
|
|
178
|
-
standardHeaders: boolean;
|
|
187
|
+
standardHeaders: boolean | DraftHeadersVersion;
|
|
179
188
|
/**
|
|
180
189
|
* The name of the property on the request object to store the rate limit info.
|
|
181
190
|
*
|
|
@@ -309,6 +318,11 @@ export declare class MemoryStore implements Store {
|
|
|
309
318
|
* Reference to the active timer.
|
|
310
319
|
*/
|
|
311
320
|
interval?: NodeJS.Timer;
|
|
321
|
+
/**
|
|
322
|
+
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
323
|
+
* cannot affect other instances.
|
|
324
|
+
*/
|
|
325
|
+
localKeys: boolean;
|
|
312
326
|
/**
|
|
313
327
|
* Method that initializes the store.
|
|
314
328
|
*
|
package/dist/index.d.mts
CHANGED
|
@@ -130,7 +130,16 @@ export type Store = {
|
|
|
130
130
|
* Method to shutdown the store, stop timers, and release all resources.
|
|
131
131
|
*/
|
|
132
132
|
shutdown?: () => Promise<void> | void;
|
|
133
|
+
/**
|
|
134
|
+
* Flag to indicate that keys incremented in one instance of this store can
|
|
135
|
+
* not affect other instances. Typically false if a database is used, true for
|
|
136
|
+
* MemoryStore.
|
|
137
|
+
*
|
|
138
|
+
* Used to help detect double-counting misconfigurations.
|
|
139
|
+
*/
|
|
140
|
+
localKeys?: boolean;
|
|
133
141
|
};
|
|
142
|
+
export type DraftHeadersVersion = "draft-6" | "draft-7";
|
|
134
143
|
/**
|
|
135
144
|
* The configuration options for the rate limiter.
|
|
136
145
|
*/
|
|
@@ -175,7 +184,7 @@ export type Options = {
|
|
|
175
184
|
*
|
|
176
185
|
* Defaults to `false` (for backward compatibility, but its use is recommended).
|
|
177
186
|
*/
|
|
178
|
-
standardHeaders: boolean;
|
|
187
|
+
standardHeaders: boolean | DraftHeadersVersion;
|
|
179
188
|
/**
|
|
180
189
|
* The name of the property on the request object to store the rate limit info.
|
|
181
190
|
*
|
|
@@ -309,6 +318,11 @@ export declare class MemoryStore implements Store {
|
|
|
309
318
|
* Reference to the active timer.
|
|
310
319
|
*/
|
|
311
320
|
interval?: NodeJS.Timer;
|
|
321
|
+
/**
|
|
322
|
+
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
323
|
+
* cannot affect other instances.
|
|
324
|
+
*/
|
|
325
|
+
localKeys: boolean;
|
|
312
326
|
/**
|
|
313
327
|
* Method that initializes the store.
|
|
314
328
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -130,7 +130,16 @@ export type Store = {
|
|
|
130
130
|
* Method to shutdown the store, stop timers, and release all resources.
|
|
131
131
|
*/
|
|
132
132
|
shutdown?: () => Promise<void> | void;
|
|
133
|
+
/**
|
|
134
|
+
* Flag to indicate that keys incremented in one instance of this store can
|
|
135
|
+
* not affect other instances. Typically false if a database is used, true for
|
|
136
|
+
* MemoryStore.
|
|
137
|
+
*
|
|
138
|
+
* Used to help detect double-counting misconfigurations.
|
|
139
|
+
*/
|
|
140
|
+
localKeys?: boolean;
|
|
133
141
|
};
|
|
142
|
+
export type DraftHeadersVersion = "draft-6" | "draft-7";
|
|
134
143
|
/**
|
|
135
144
|
* The configuration options for the rate limiter.
|
|
136
145
|
*/
|
|
@@ -175,7 +184,7 @@ export type Options = {
|
|
|
175
184
|
*
|
|
176
185
|
* Defaults to `false` (for backward compatibility, but its use is recommended).
|
|
177
186
|
*/
|
|
178
|
-
standardHeaders: boolean;
|
|
187
|
+
standardHeaders: boolean | DraftHeadersVersion;
|
|
179
188
|
/**
|
|
180
189
|
* The name of the property on the request object to store the rate limit info.
|
|
181
190
|
*
|
|
@@ -309,6 +318,11 @@ export declare class MemoryStore implements Store {
|
|
|
309
318
|
* Reference to the active timer.
|
|
310
319
|
*/
|
|
311
320
|
interval?: NodeJS.Timer;
|
|
321
|
+
/**
|
|
322
|
+
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
323
|
+
* cannot affect other instances.
|
|
324
|
+
*/
|
|
325
|
+
localKeys: boolean;
|
|
312
326
|
/**
|
|
313
327
|
* Method that initializes the store.
|
|
314
328
|
*
|
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,59 @@ var __publicField = (obj, key, value) => {
|
|
|
5
5
|
return value;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
// source/headers.ts
|
|
9
|
+
var getResetSeconds = (resetTime, windowMs) => {
|
|
10
|
+
let resetSeconds = void 0;
|
|
11
|
+
if (resetTime) {
|
|
12
|
+
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
|
13
|
+
resetSeconds = Math.max(0, deltaSeconds);
|
|
14
|
+
} else if (windowMs) {
|
|
15
|
+
resetSeconds = Math.ceil(windowMs / 1e3);
|
|
16
|
+
}
|
|
17
|
+
return resetSeconds;
|
|
18
|
+
};
|
|
19
|
+
var setLegacyHeaders = (response, info) => {
|
|
20
|
+
if (response.headersSent)
|
|
21
|
+
return;
|
|
22
|
+
response.setHeader("X-RateLimit-Limit", info.limit);
|
|
23
|
+
response.setHeader("X-RateLimit-Remaining", info.remaining);
|
|
24
|
+
if (info.resetTime instanceof Date) {
|
|
25
|
+
response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
|
|
26
|
+
response.setHeader(
|
|
27
|
+
"X-RateLimit-Reset",
|
|
28
|
+
Math.ceil(info.resetTime.getTime() / 1e3)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var setDraft6Headers = (response, info, windowMs) => {
|
|
33
|
+
if (response.headersSent)
|
|
34
|
+
return;
|
|
35
|
+
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
36
|
+
const resetSeconds = getResetSeconds(info.resetTime);
|
|
37
|
+
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
38
|
+
response.setHeader("RateLimit-Limit", info.limit);
|
|
39
|
+
response.setHeader("RateLimit-Remaining", info.remaining);
|
|
40
|
+
if (resetSeconds)
|
|
41
|
+
response.setHeader("RateLimit-Reset", resetSeconds);
|
|
42
|
+
};
|
|
43
|
+
var setDraft7Headers = (response, info, windowMs) => {
|
|
44
|
+
if (response.headersSent)
|
|
45
|
+
return;
|
|
46
|
+
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
47
|
+
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
48
|
+
response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
49
|
+
response.setHeader(
|
|
50
|
+
"RateLimit",
|
|
51
|
+
`limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
var setRetryAfterHeader = (response, info, windowMs) => {
|
|
55
|
+
if (response.headersSent)
|
|
56
|
+
return;
|
|
57
|
+
const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
|
58
|
+
response.setHeader("Retry-After", resetSeconds);
|
|
59
|
+
};
|
|
60
|
+
|
|
8
61
|
// source/validations.ts
|
|
9
62
|
import { isIP } from "net";
|
|
10
63
|
var ValidationError = class extends Error {
|
|
@@ -12,21 +65,23 @@ var ValidationError = class extends Error {
|
|
|
12
65
|
* The code must be a string, in snake case and all capital, that starts with
|
|
13
66
|
* the substring `ERR_ERL_`.
|
|
14
67
|
*
|
|
15
|
-
* The message must be a string, starting with
|
|
68
|
+
* The message must be a string, starting with an uppercase character,
|
|
16
69
|
* describing the issue in detail.
|
|
17
70
|
*/
|
|
18
71
|
constructor(code, message) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
);
|
|
72
|
+
const url = `https://express-rate-limit.github.io/${code}/`;
|
|
73
|
+
super(`${message} See ${url} for more information.`);
|
|
22
74
|
__publicField(this, "name");
|
|
23
75
|
__publicField(this, "code");
|
|
76
|
+
__publicField(this, "help");
|
|
24
77
|
this.name = this.constructor.name;
|
|
25
78
|
this.code = code;
|
|
26
|
-
this.
|
|
79
|
+
this.help = url;
|
|
27
80
|
}
|
|
28
81
|
};
|
|
29
|
-
var
|
|
82
|
+
var ChangeWarning = class extends ValidationError {
|
|
83
|
+
};
|
|
84
|
+
var _Validations = class _Validations {
|
|
30
85
|
constructor(enabled) {
|
|
31
86
|
// eslint-disable-next-line @typescript-eslint/parameter-properties
|
|
32
87
|
__publicField(this, "enabled");
|
|
@@ -103,6 +158,125 @@ var Validations = class {
|
|
|
103
158
|
}
|
|
104
159
|
});
|
|
105
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Ensures totalHits value from store is a positive integer.
|
|
163
|
+
*
|
|
164
|
+
* @param hits {any} - The `totalHits` returned by the store.
|
|
165
|
+
*/
|
|
166
|
+
positiveHits(hits) {
|
|
167
|
+
this.wrap(() => {
|
|
168
|
+
if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
|
|
169
|
+
throw new ValidationError(
|
|
170
|
+
"ERR_ERL_INVALID_HITS",
|
|
171
|
+
`The totalHits value returned from the store must be a positive integer, got ${hits}`
|
|
172
|
+
// eslint-disable-line @typescript-eslint/restrict-template-expressions
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Ensures a given key is incremented only once per request.
|
|
179
|
+
*
|
|
180
|
+
* @param request {Request} - The Express request object.
|
|
181
|
+
* @param store {Store} - The store class.
|
|
182
|
+
* @param key {string} - The key used to store the client's hit count.
|
|
183
|
+
*
|
|
184
|
+
* @returns {void}
|
|
185
|
+
*/
|
|
186
|
+
singleCount(request, store, key) {
|
|
187
|
+
this.wrap(() => {
|
|
188
|
+
let storeKeys = _Validations.singleCountKeys.get(request);
|
|
189
|
+
if (!storeKeys) {
|
|
190
|
+
storeKeys = /* @__PURE__ */ new Map();
|
|
191
|
+
_Validations.singleCountKeys.set(request, storeKeys);
|
|
192
|
+
}
|
|
193
|
+
const storeKey = store.localKeys ? store : store.constructor.name;
|
|
194
|
+
let keys = storeKeys.get(storeKey);
|
|
195
|
+
if (!keys) {
|
|
196
|
+
keys = [];
|
|
197
|
+
storeKeys.set(storeKey, keys);
|
|
198
|
+
}
|
|
199
|
+
if (keys.includes(key)) {
|
|
200
|
+
throw new ValidationError(
|
|
201
|
+
"ERR_ERL_DOUBLE_COUNT",
|
|
202
|
+
`The hit count for ${key} was incremented more than once for a single request.`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
keys.push(key);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Warns the user that the behaviour for `max: 0` is changing in the next
|
|
210
|
+
* major release.
|
|
211
|
+
*
|
|
212
|
+
* @param max {number} - The maximum number of hits per client.
|
|
213
|
+
*
|
|
214
|
+
* @returns {void}
|
|
215
|
+
*/
|
|
216
|
+
max(max) {
|
|
217
|
+
this.wrap(() => {
|
|
218
|
+
if (max === 0) {
|
|
219
|
+
throw new ChangeWarning(
|
|
220
|
+
"WRN_ERL_MAX_ZERO",
|
|
221
|
+
`Setting max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
|
|
228
|
+
* and will be removed in the next major release.
|
|
229
|
+
*
|
|
230
|
+
* @param draft_polli_ratelimit_headers {boolean|undefined} - The now-deprecated setting that was used to enable standard headers.
|
|
231
|
+
*
|
|
232
|
+
* @returns {void}
|
|
233
|
+
*/
|
|
234
|
+
draftPolliHeaders(draft_polli_ratelimit_headers) {
|
|
235
|
+
this.wrap(() => {
|
|
236
|
+
if (draft_polli_ratelimit_headers) {
|
|
237
|
+
throw new ChangeWarning(
|
|
238
|
+
"WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
|
|
239
|
+
`The draft_polli_ratelimit_headers configuration option is deprecated and will be removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Warns the user that the `onLimitReached` option is deprecated and will be removed in the next
|
|
246
|
+
* major release.
|
|
247
|
+
*
|
|
248
|
+
* @param onLimitReached {function|undefined} - The maximum number of hits per client.
|
|
249
|
+
*
|
|
250
|
+
* @returns {void}
|
|
251
|
+
*/
|
|
252
|
+
onLimitReached(onLimitReached) {
|
|
253
|
+
this.wrap(() => {
|
|
254
|
+
if (onLimitReached) {
|
|
255
|
+
throw new ChangeWarning(
|
|
256
|
+
"WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
|
|
257
|
+
`The onLimitReached configuration option is deprecated and will be removed in express-rate-limit v7.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Warns the user when the selected headers option requires a reset time but
|
|
264
|
+
* the store does not provide one.
|
|
265
|
+
*
|
|
266
|
+
* @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
|
|
267
|
+
*
|
|
268
|
+
* @returns {void}
|
|
269
|
+
*/
|
|
270
|
+
headersResetTime(resetTime) {
|
|
271
|
+
this.wrap(() => {
|
|
272
|
+
if (!resetTime) {
|
|
273
|
+
throw new ValidationError(
|
|
274
|
+
"ERR_ERL_HEADERS_NO_RESET",
|
|
275
|
+
`standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
106
280
|
wrap(validation) {
|
|
107
281
|
if (!this.enabled) {
|
|
108
282
|
return;
|
|
@@ -110,10 +284,25 @@ var Validations = class {
|
|
|
110
284
|
try {
|
|
111
285
|
validation.call(this);
|
|
112
286
|
} catch (error) {
|
|
113
|
-
|
|
287
|
+
if (error instanceof ChangeWarning)
|
|
288
|
+
console.warn(error);
|
|
289
|
+
else
|
|
290
|
+
console.error(error);
|
|
114
291
|
}
|
|
115
292
|
}
|
|
116
293
|
};
|
|
294
|
+
/**
|
|
295
|
+
* Maps the key used in a store for a certain request, and ensures that the
|
|
296
|
+
* same key isn't used more than once per request.
|
|
297
|
+
*
|
|
298
|
+
* The store can be any one of the following:
|
|
299
|
+
* - An instance, for stores like the MemoryStore where two instances do not
|
|
300
|
+
* share state.
|
|
301
|
+
* - A string (class name), for stores where multiple instances
|
|
302
|
+
* typically share state, such as the Redis store.
|
|
303
|
+
*/
|
|
304
|
+
__publicField(_Validations, "singleCountKeys", /* @__PURE__ */ new WeakMap());
|
|
305
|
+
var Validations = _Validations;
|
|
117
306
|
|
|
118
307
|
// source/memory-store.ts
|
|
119
308
|
var calculateNextResetTime = (windowMs) => {
|
|
@@ -139,6 +328,11 @@ var MemoryStore = class {
|
|
|
139
328
|
* Reference to the active timer.
|
|
140
329
|
*/
|
|
141
330
|
__publicField(this, "interval");
|
|
331
|
+
/**
|
|
332
|
+
* Confirmation that the keys incremented in once instance of MemoryStore
|
|
333
|
+
* cannot affect other instances.
|
|
334
|
+
*/
|
|
335
|
+
__publicField(this, "localKeys", true);
|
|
142
336
|
}
|
|
143
337
|
/**
|
|
144
338
|
* Method that initializes the store.
|
|
@@ -245,6 +439,7 @@ var promisifyStore = (passedStore) => {
|
|
|
245
439
|
async resetKey(key) {
|
|
246
440
|
return legacyStore.resetKey(key);
|
|
247
441
|
}
|
|
442
|
+
/* istanbul ignore next */
|
|
248
443
|
async resetAll() {
|
|
249
444
|
if (typeof legacyStore.resetAll === "function")
|
|
250
445
|
return legacyStore.resetAll();
|
|
@@ -273,13 +468,20 @@ var parseOptions = (passedOptions) => {
|
|
|
273
468
|
var _a, _b, _c, _d;
|
|
274
469
|
const notUndefinedOptions = omitUndefinedOptions(passedOptions);
|
|
275
470
|
const validations = new Validations((_a = notUndefinedOptions == null ? void 0 : notUndefinedOptions.validate) != null ? _a : true);
|
|
471
|
+
validations.draftPolliHeaders(
|
|
472
|
+
notUndefinedOptions.draft_polli_ratelimit_headers
|
|
473
|
+
);
|
|
474
|
+
validations.onLimitReached(notUndefinedOptions.onLimitReached);
|
|
475
|
+
let standardHeaders = (_b = notUndefinedOptions.standardHeaders) != null ? _b : false;
|
|
476
|
+
if (standardHeaders === true || standardHeaders === void 0 && notUndefinedOptions.draft_polli_ratelimit_headers) {
|
|
477
|
+
standardHeaders = "draft-6";
|
|
478
|
+
}
|
|
276
479
|
const config = {
|
|
277
480
|
windowMs: 60 * 1e3,
|
|
278
481
|
max: 5,
|
|
279
482
|
message: "Too many requests, please try again later.",
|
|
280
483
|
statusCode: 429,
|
|
281
|
-
legacyHeaders: (
|
|
282
|
-
standardHeaders: (_c = passedOptions.draft_polli_ratelimit_headers) != null ? _c : false,
|
|
484
|
+
legacyHeaders: (_c = passedOptions.headers) != null ? _c : true,
|
|
283
485
|
requestPropertyName: "rateLimit",
|
|
284
486
|
skipFailedRequests: false,
|
|
285
487
|
skipSuccessfulRequests: false,
|
|
@@ -298,13 +500,15 @@ var parseOptions = (passedOptions) => {
|
|
|
298
500
|
response
|
|
299
501
|
) : config.message;
|
|
300
502
|
if (!response.writableEnded) {
|
|
301
|
-
response.send(message
|
|
503
|
+
response.send(message);
|
|
302
504
|
}
|
|
303
505
|
},
|
|
304
506
|
onLimitReached(_request, _response, _optionsUsed) {
|
|
305
507
|
},
|
|
306
|
-
// Allow the options
|
|
508
|
+
// Allow the default options to be overriden by the options passed to the middleware.
|
|
307
509
|
...notUndefinedOptions,
|
|
510
|
+
// `standardHeaders` is resolved into a draft version above, use that.
|
|
511
|
+
standardHeaders,
|
|
308
512
|
// Note that this field is declared after the user's options are spread in,
|
|
309
513
|
// so that this field doesn't get overriden with an un-promisified store!
|
|
310
514
|
store: promisifyStore((_d = notUndefinedOptions.store) != null ? _d : new MemoryStore()),
|
|
@@ -340,39 +544,27 @@ var rateLimit = (passedOptions) => {
|
|
|
340
544
|
const augmentedRequest = request;
|
|
341
545
|
const key = await config.keyGenerator(request, response);
|
|
342
546
|
const { totalHits, resetTime } = await config.store.increment(key);
|
|
547
|
+
config.validations.positiveHits(totalHits);
|
|
548
|
+
config.validations.singleCount(request, config.store, key);
|
|
343
549
|
const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max;
|
|
344
550
|
const maxHits = await retrieveQuota;
|
|
345
|
-
|
|
551
|
+
config.validations.max(maxHits);
|
|
552
|
+
const info = {
|
|
346
553
|
limit: maxHits,
|
|
347
554
|
current: totalHits,
|
|
348
555
|
remaining: Math.max(maxHits - totalHits, 0),
|
|
349
556
|
resetTime
|
|
350
557
|
};
|
|
558
|
+
augmentedRequest[config.requestPropertyName] = info;
|
|
351
559
|
if (config.legacyHeaders && !response.headersSent) {
|
|
352
|
-
response
|
|
353
|
-
response.setHeader(
|
|
354
|
-
"X-RateLimit-Remaining",
|
|
355
|
-
augmentedRequest[config.requestPropertyName].remaining
|
|
356
|
-
);
|
|
357
|
-
if (resetTime instanceof Date) {
|
|
358
|
-
response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
|
|
359
|
-
response.setHeader(
|
|
360
|
-
"X-RateLimit-Reset",
|
|
361
|
-
Math.ceil(resetTime.getTime() / 1e3)
|
|
362
|
-
);
|
|
363
|
-
}
|
|
560
|
+
setLegacyHeaders(response, info);
|
|
364
561
|
}
|
|
365
562
|
if (config.standardHeaders && !response.headersSent) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (resetTime) {
|
|
372
|
-
const deltaSeconds = Math.ceil(
|
|
373
|
-
(resetTime.getTime() - Date.now()) / 1e3
|
|
374
|
-
);
|
|
375
|
-
response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
|
|
563
|
+
if (config.standardHeaders === "draft-6") {
|
|
564
|
+
setDraft6Headers(response, info, config.windowMs);
|
|
565
|
+
} else if (config.standardHeaders === "draft-7") {
|
|
566
|
+
config.validations.headersResetTime(info.resetTime);
|
|
567
|
+
setDraft7Headers(response, info, config.windowMs);
|
|
376
568
|
}
|
|
377
569
|
}
|
|
378
570
|
if (config.skipFailedRequests || config.skipSuccessfulRequests) {
|
|
@@ -408,8 +600,8 @@ var rateLimit = (passedOptions) => {
|
|
|
408
600
|
}
|
|
409
601
|
config.validations.disable();
|
|
410
602
|
if (maxHits && totalHits > maxHits) {
|
|
411
|
-
if (
|
|
412
|
-
response
|
|
603
|
+
if (config.legacyHeaders || config.standardHeaders) {
|
|
604
|
+
setRetryAfterHeader(response, info, config.windowMs);
|
|
413
605
|
}
|
|
414
606
|
config.handler(request, response, next, options);
|
|
415
607
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-rate-limit",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.10.0",
|
|
4
4
|
"description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Nathan Friedly",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"changelog.md"
|
|
53
53
|
],
|
|
54
54
|
"engines": {
|
|
55
|
-
"node": ">= 14
|
|
55
|
+
"node": ">= 14"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz",
|
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
"lint:code": "xo --ignore test/external/",
|
|
64
64
|
"lint:rest": "prettier --ignore-path .gitignore --ignore-unknown --check .",
|
|
65
65
|
"lint": "run-s lint:*",
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"test:lib": "cross-env NODE_NO_WARNINGS=1 NODE_OPTIONS=--experimental-vm-modules jest",
|
|
66
|
+
"format:code": "npm run lint:code -- --fix",
|
|
67
|
+
"format:rest": "npm run lint:rest -- --write .",
|
|
68
|
+
"format": "run-s format:*",
|
|
69
|
+
"test:lib": "cross-env NODE_NO_WARNINGS=1 NODE_OPTIONS=--experimental-vm-modules jest --config config/jest.json",
|
|
70
70
|
"test:ext": "cd test/external/ && bash run-all-tests",
|
|
71
|
-
"test": "run-s lint test
|
|
71
|
+
"test": "run-s lint test:lib",
|
|
72
72
|
"pre-commit": "lint-staged",
|
|
73
73
|
"prepare": "run-s compile && husky install config/husky"
|
|
74
74
|
},
|
|
@@ -76,9 +76,11 @@
|
|
|
76
76
|
"express": "^4 || ^5"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@
|
|
79
|
+
"@express-rate-limit/prettier": "1.0.0",
|
|
80
|
+
"@express-rate-limit/tsconfig": "1.0.0",
|
|
81
|
+
"@jest/globals": "29.6.2",
|
|
80
82
|
"@types/express": "4.17.17",
|
|
81
|
-
"@types/jest": "29.5.
|
|
83
|
+
"@types/jest": "29.5.3",
|
|
82
84
|
"@types/node": "20.4.0",
|
|
83
85
|
"@types/supertest": "2.0.12",
|
|
84
86
|
"cross-env": "7.0.3",
|
|
@@ -87,9 +89,10 @@
|
|
|
87
89
|
"esbuild": "0.18.11",
|
|
88
90
|
"express": "4.18.2",
|
|
89
91
|
"husky": "8.0.3",
|
|
90
|
-
"jest": "29.6.
|
|
92
|
+
"jest": "29.6.2",
|
|
91
93
|
"lint-staged": "13.2.3",
|
|
92
94
|
"npm-run-all": "4.1.5",
|
|
95
|
+
"ratelimit-header-parser": "0.1.0",
|
|
93
96
|
"supertest": "6.3.3",
|
|
94
97
|
"ts-jest": "29.1.1",
|
|
95
98
|
"ts-node": "10.9.1",
|
|
@@ -112,40 +115,13 @@
|
|
|
112
115
|
{
|
|
113
116
|
"files": "test/library/*.ts",
|
|
114
117
|
"rules": {
|
|
115
|
-
"@typescript-eslint/no-unsafe-argument": 0
|
|
118
|
+
"@typescript-eslint/no-unsafe-argument": 0,
|
|
119
|
+
"@typescript-eslint/no-unsafe-assignment": 0
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
]
|
|
119
123
|
},
|
|
120
|
-
"prettier":
|
|
121
|
-
"semi": false,
|
|
122
|
-
"useTabs": true,
|
|
123
|
-
"singleQuote": true,
|
|
124
|
-
"bracketSpacing": true,
|
|
125
|
-
"trailingComma": "all",
|
|
126
|
-
"proseWrap": "always"
|
|
127
|
-
},
|
|
128
|
-
"jest": {
|
|
129
|
-
"preset": "ts-jest/presets/default-esm",
|
|
130
|
-
"collectCoverage": true,
|
|
131
|
-
"collectCoverageFrom": [
|
|
132
|
-
"source/**/*.ts"
|
|
133
|
-
],
|
|
134
|
-
"testTimeout": 30000,
|
|
135
|
-
"testMatch": [
|
|
136
|
-
"**/test/library/**/*-test.[jt]s?(x)"
|
|
137
|
-
],
|
|
138
|
-
"moduleFileExtensions": [
|
|
139
|
-
"js",
|
|
140
|
-
"jsx",
|
|
141
|
-
"json",
|
|
142
|
-
"ts",
|
|
143
|
-
"tsx"
|
|
144
|
-
],
|
|
145
|
-
"moduleNameMapper": {
|
|
146
|
-
"^(\\.{1,2}/.*)\\.js$": "$1"
|
|
147
|
-
}
|
|
148
|
-
},
|
|
124
|
+
"prettier": "@express-rate-limit/prettier",
|
|
149
125
|
"lint-staged": {
|
|
150
126
|
"{source,test}/**/*.ts": "xo --ignore test/external/ --fix",
|
|
151
127
|
"**/*.{json,yaml,md}": "prettier --ignore-path .gitignore --ignore-unknown --write "
|
package/readme.md
CHANGED
|
@@ -12,21 +12,46 @@ authentication and more to any API in minutes. Learn more at
|
|
|
12
12
|
|
|
13
13
|
<div align="center">
|
|
14
14
|
|
|
15
|
-
[](https://github.com/express-rate-limit/express-rate-limit/actions/workflows/ci.yaml)
|
|
16
16
|
[](https://npmjs.org/package/express-rate-limit 'View this project on NPM')
|
|
17
17
|
[](https://www.npmjs.com/package/express-rate-limit)
|
|
18
18
|
|
|
19
|
-
Basic rate-limiting middleware for Express. Use to
|
|
20
|
-
public APIs and/or endpoints such as password reset.
|
|
19
|
+
Basic rate-limiting middleware for [Express](http://expressjs.com/). Use to
|
|
20
|
+
limit repeated requests to public APIs and/or endpoints such as password reset.
|
|
21
|
+
Plays nice with
|
|
21
22
|
[express-slow-down](https://www.npmjs.com/package/express-slow-down).
|
|
22
23
|
|
|
23
24
|
</div>
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
## Use Cases
|
|
27
|
+
|
|
28
|
+
Depending on your use case, you may need to switch to a different
|
|
29
|
+
[store](#store).
|
|
30
|
+
|
|
31
|
+
#### Abuse Prevention
|
|
32
|
+
|
|
33
|
+
The default `MemoryStore` is probably fine.
|
|
34
|
+
|
|
35
|
+
#### API Rate Limit Enforcement
|
|
36
|
+
|
|
37
|
+
You likely want to switch to a different [store](#store). As a performance
|
|
38
|
+
optimization, the default `MemoryStore` uses a global time window, so if your
|
|
39
|
+
limit is 10 requests per minute, a single user might be able to get an initial
|
|
40
|
+
burst of up to 20 requests in a row if they happen to get the first 10 in at the
|
|
41
|
+
end of one minute and the next 10 in at the start of the next minute. (After the
|
|
42
|
+
initial burst, they will be limited to the expected 10 requests per minute.) All
|
|
43
|
+
other stores use per-user time windows, so a user will get exactly 10 requests
|
|
44
|
+
regardless.
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
Additionally, if you have multiple servers or processes (for example, with the
|
|
47
|
+
[node:cluster](https://nodejs.org/api/cluster.html) module), you'll likely want
|
|
48
|
+
to use an external data store to syhcnronize hits
|
|
49
|
+
([redis](https://npmjs.com/package/rate-limit-redis),
|
|
50
|
+
[memcached](https://npmjs.org/package/rate-limit-memcached), [etc.](#store))
|
|
51
|
+
This will guarentee the expected result even if some requests get handled by
|
|
52
|
+
different servers/processes.
|
|
53
|
+
|
|
54
|
+
### Alternate Rate Limiters
|
|
30
55
|
|
|
31
56
|
This module was designed to only handle the basics and didn't even support
|
|
32
57
|
external stores initially. These other options all are excellent pieces of
|
|
@@ -72,13 +97,13 @@ Import it in a CommonJS project (`type: commonjs` or no `type` field in
|
|
|
72
97
|
`package.json`) as follows:
|
|
73
98
|
|
|
74
99
|
```ts
|
|
75
|
-
const rateLimit = require('express-rate-limit')
|
|
100
|
+
const { rateLimit } = require('express-rate-limit')
|
|
76
101
|
```
|
|
77
102
|
|
|
78
103
|
Import it in a ESM project (`type: module` in `package.json`) as follows:
|
|
79
104
|
|
|
80
105
|
```ts
|
|
81
|
-
import rateLimit from 'express-rate-limit'
|
|
106
|
+
import { rateLimit } from 'express-rate-limit'
|
|
82
107
|
```
|
|
83
108
|
|
|
84
109
|
### Examples
|
|
@@ -87,13 +112,14 @@ To use it in an API-only server where the rate-limiter should be applied to all
|
|
|
87
112
|
requests:
|
|
88
113
|
|
|
89
114
|
```ts
|
|
90
|
-
import rateLimit from 'express-rate-limit'
|
|
115
|
+
import { rateLimit } from 'express-rate-limit'
|
|
91
116
|
|
|
92
117
|
const limiter = rateLimit({
|
|
93
118
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
94
119
|
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
95
|
-
standardHeaders:
|
|
96
|
-
legacyHeaders: false, //
|
|
120
|
+
standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
|
|
121
|
+
legacyHeaders: false, // X-RateLimit-* headers
|
|
122
|
+
// store: ... , // Use an external store for more precise rate limiting
|
|
97
123
|
})
|
|
98
124
|
|
|
99
125
|
// Apply the rate limiting middleware to all requests
|
|
@@ -105,13 +131,14 @@ To use it in a 'regular' web server (e.g. anything that uses
|
|
|
105
131
|
requests:
|
|
106
132
|
|
|
107
133
|
```ts
|
|
108
|
-
import rateLimit from 'express-rate-limit'
|
|
134
|
+
import { rateLimit } from 'express-rate-limit'
|
|
109
135
|
|
|
110
136
|
const apiLimiter = rateLimit({
|
|
111
137
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
112
138
|
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
113
|
-
standardHeaders:
|
|
139
|
+
standardHeaders: 'draft-7', // Set `RateLimit` and `RateLimit-Policy`` headers
|
|
114
140
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
|
141
|
+
// store: ... , // Use an external store for more precise rate limiting
|
|
115
142
|
})
|
|
116
143
|
|
|
117
144
|
// Apply the rate limiting middleware to API calls only
|
|
@@ -121,13 +148,14 @@ app.use('/api', apiLimiter)
|
|
|
121
148
|
To create multiple instances to apply different rules to different endpoints:
|
|
122
149
|
|
|
123
150
|
```ts
|
|
124
|
-
import rateLimit from 'express-rate-limit'
|
|
151
|
+
import { rateLimit } from 'express-rate-limit'
|
|
125
152
|
|
|
126
153
|
const apiLimiter = rateLimit({
|
|
127
154
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
128
155
|
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
129
|
-
standardHeaders:
|
|
130
|
-
legacyHeaders: false, //
|
|
156
|
+
standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
|
|
157
|
+
legacyHeaders: false, // X-RateLimit-* headers
|
|
158
|
+
// store: ... , // Use an external store for more precise rate limiting
|
|
131
159
|
})
|
|
132
160
|
|
|
133
161
|
app.use('/api/', apiLimiter)
|
|
@@ -137,8 +165,8 @@ const createAccountLimiter = rateLimit({
|
|
|
137
165
|
max: 5, // Limit each IP to 5 create account requests per `window` (here, per hour)
|
|
138
166
|
message:
|
|
139
167
|
'Too many accounts created from this IP, please try again after an hour',
|
|
140
|
-
standardHeaders:
|
|
141
|
-
legacyHeaders: false, //
|
|
168
|
+
standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
|
|
169
|
+
legacyHeaders: false, // X-RateLimit-* headers
|
|
142
170
|
})
|
|
143
171
|
|
|
144
172
|
app.post('/create-account', createAccountLimiter, (request, response) => {
|
|
@@ -149,17 +177,23 @@ app.post('/create-account', createAccountLimiter, (request, response) => {
|
|
|
149
177
|
To use a custom store:
|
|
150
178
|
|
|
151
179
|
```ts
|
|
152
|
-
import
|
|
180
|
+
import { rateLimit } from 'express-rate-limit'
|
|
181
|
+
import RedisStore from 'rate-limit-redis'
|
|
182
|
+
import RedisClient from 'ioredis'
|
|
153
183
|
|
|
154
|
-
const
|
|
184
|
+
const redisClient = new RedisClient()
|
|
185
|
+
const rateLimiter = rateLimit({
|
|
155
186
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
156
187
|
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
157
|
-
standardHeaders:
|
|
158
|
-
|
|
188
|
+
standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
|
|
189
|
+
legacyHeaders: false, // X-RateLimit-* headers
|
|
190
|
+
store: new RedisStore({
|
|
191
|
+
/* ... */
|
|
192
|
+
}), // Use the external store
|
|
159
193
|
})
|
|
160
194
|
|
|
161
|
-
// Apply the rate limiting middleware to
|
|
162
|
-
app.use(
|
|
195
|
+
// Apply the rate limiting middleware to all requests
|
|
196
|
+
app.use(rateLimiter)
|
|
163
197
|
```
|
|
164
198
|
|
|
165
199
|
> **Note:** most stores will require additional configuration, such as custom
|
|
@@ -296,16 +330,38 @@ Defaults to `true` (for backward compatibility).
|
|
|
296
330
|
|
|
297
331
|
### `standardHeaders`
|
|
298
332
|
|
|
299
|
-
> `boolean`
|
|
333
|
+
> `boolean` | `'draft-6'` | `'draft-7'`
|
|
300
334
|
|
|
301
335
|
Whether to enable support for headers conforming to the
|
|
302
|
-
[
|
|
303
|
-
adopted by the IETF
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
336
|
+
[RateLimit header fields for HTTP standardization draft](https://github.com/ietf-wg-httpapi/ratelimit-headers)
|
|
337
|
+
adopted by the IETF.
|
|
338
|
+
|
|
339
|
+
If set to `draft-6`, separate `RateLimit-Policy` `RateLimit-Limit`,
|
|
340
|
+
`RateLimit-Remaining`, and, if the store supports it, `RateLimit-Reset` headers
|
|
341
|
+
are set on the response, in accordance with
|
|
342
|
+
[draft-ietf-httpapi-ratelimit-headers-06](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-06).
|
|
343
|
+
|
|
344
|
+
If set to `draft-7`, a combined `RateLimit` header is set containing limit,
|
|
345
|
+
remaining, and reset values, and a `RateLimit-Policy` header is set, in
|
|
346
|
+
accordiance with
|
|
347
|
+
[draft-ietf-httpapi-ratelimit-headers-07](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-07).
|
|
348
|
+
`windowMs` is used for the reset value if the store does not provide a reset
|
|
349
|
+
time.
|
|
307
350
|
|
|
308
|
-
|
|
351
|
+
If set to `true`, it is treated as `draft-6`, however this behavior may change
|
|
352
|
+
in a future semver major release.
|
|
353
|
+
|
|
354
|
+
If set to any truthy value, the middleware also sends the `Retry-After` header
|
|
355
|
+
on all blocked requests.
|
|
356
|
+
|
|
357
|
+
The `standardHeaders` option may be used in conjunction with, or instead of the
|
|
358
|
+
`legacyHeaders` option.
|
|
359
|
+
|
|
360
|
+
ℹ️ Tip: use
|
|
361
|
+
[ratelimit-header-parser](https://www.npmjs.com/package/ratelimit-header-parser)
|
|
362
|
+
in clients to read/parse any form of express-rate-limit's headers.
|
|
363
|
+
|
|
364
|
+
Defaults to `false`.
|
|
309
365
|
|
|
310
366
|
> Renamed in `6.x` from `draft_polli_ratelimit_headers` to `standardHeaders`.
|
|
311
367
|
|
|
@@ -397,8 +453,8 @@ const limiter = rateLimit({
|
|
|
397
453
|
> `function`
|
|
398
454
|
|
|
399
455
|
A (sync/async) function that accepts the Express `request` and `response`
|
|
400
|
-
objects that is called
|
|
401
|
-
rate
|
|
456
|
+
objects that is called the on the request where a client has just exceeded their
|
|
457
|
+
rate limit.
|
|
402
458
|
|
|
403
459
|
This method was
|
|
404
460
|
[deprecated in v6](https://github.com/express-rate-limit/express-rate-limit/releases/v6.0.0) -
|
|
@@ -482,7 +538,7 @@ Here is a list of external stores:
|
|
|
482
538
|
| [`rate-limit-redis`](https://npmjs.com/package/rate-limit-redis) | A [Redis](http://redis.io/)-backed store, more suitable for large or demanding deployments. | Modern as of v3.0.0 |
|
|
483
539
|
| [`rate-limit-memcached`](https://npmjs.org/package/rate-limit-memcached) | A [Memcached](https://memcached.org/)-backed store. | Legacy |
|
|
484
540
|
| [`rate-limit-mongo`](https://www.npmjs.com/package/rate-limit-mongo) | A [MongoDB](https://www.mongodb.com/)-backed store. | Legacy |
|
|
485
|
-
| [`precise-memory-rate-limit`](https://www.npmjs.com/package/precise-memory-rate-limit) | A memory store similar to the built-in one, except that it stores a distinct timestamp for each key. |
|
|
541
|
+
| [`precise-memory-rate-limit`](https://www.npmjs.com/package/precise-memory-rate-limit) | A memory store similar to the built-in one, except that it stores a distinct timestamp for each key. | Modern as of v2.0.0 |
|
|
486
542
|
|
|
487
543
|
Take a look at
|
|
488
544
|
[this guide](https://github.com/express-rate-limit/express-rate-limit/wiki/Creating-Your-Own-Store)
|
package/tsconfig.json
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"include": ["source/"],
|
|
3
3
|
"exclude": ["node_modules/"],
|
|
4
|
-
"
|
|
5
|
-
"declaration": true,
|
|
6
|
-
"strict": true,
|
|
7
|
-
"noUnusedLocals": true,
|
|
8
|
-
"noImplicitReturns": true,
|
|
9
|
-
"noFallthroughCasesInSwitch": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"moduleResolution": "node"
|
|
12
|
-
}
|
|
4
|
+
"extends": "@express-rate-limit/tsconfig"
|
|
13
5
|
}
|