express-rate-limit 6.9.0 → 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 +16 -0
- package/dist/index.cjs +175 -32
- package/dist/index.d.cts +2 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +175 -32
- package/package.json +12 -36
- package/readme.md +48 -25
- package/tsconfig.json +1 -9
package/changelog.md
CHANGED
|
@@ -6,6 +6,22 @@ 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
|
+
|
|
9
25
|
## [6.9.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v6.9.0)
|
|
10
26
|
|
|
11
27
|
### Added
|
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,12 +91,12 @@ 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
98
|
const url = `https://express-rate-limit.github.io/${code}/`;
|
|
46
|
-
super(`${message} See ${url} for more information
|
|
99
|
+
super(`${message} See ${url} for more information.`);
|
|
47
100
|
__publicField(this, "name");
|
|
48
101
|
__publicField(this, "code");
|
|
49
102
|
__publicField(this, "help");
|
|
@@ -52,6 +105,8 @@ var ValidationError = class extends Error {
|
|
|
52
105
|
this.help = url;
|
|
53
106
|
}
|
|
54
107
|
};
|
|
108
|
+
var ChangeWarning = class extends ValidationError {
|
|
109
|
+
};
|
|
55
110
|
var _Validations = class _Validations {
|
|
56
111
|
constructor(enabled) {
|
|
57
112
|
// eslint-disable-next-line @typescript-eslint/parameter-properties
|
|
@@ -129,6 +184,22 @@ var _Validations = class _Validations {
|
|
|
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
|
+
}
|
|
132
203
|
/**
|
|
133
204
|
* Ensures a given key is incremented only once per request.
|
|
134
205
|
*
|
|
@@ -160,6 +231,78 @@ var _Validations = class _Validations {
|
|
|
160
231
|
keys.push(key);
|
|
161
232
|
});
|
|
162
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
|
+
}
|
|
163
306
|
wrap(validation) {
|
|
164
307
|
if (!this.enabled) {
|
|
165
308
|
return;
|
|
@@ -167,7 +310,10 @@ var _Validations = class _Validations {
|
|
|
167
310
|
try {
|
|
168
311
|
validation.call(this);
|
|
169
312
|
} catch (error) {
|
|
170
|
-
|
|
313
|
+
if (error instanceof ChangeWarning)
|
|
314
|
+
console.warn(error);
|
|
315
|
+
else
|
|
316
|
+
console.error(error);
|
|
171
317
|
}
|
|
172
318
|
}
|
|
173
319
|
};
|
|
@@ -319,6 +465,7 @@ var promisifyStore = (passedStore) => {
|
|
|
319
465
|
async resetKey(key) {
|
|
320
466
|
return legacyStore.resetKey(key);
|
|
321
467
|
}
|
|
468
|
+
/* istanbul ignore next */
|
|
322
469
|
async resetAll() {
|
|
323
470
|
if (typeof legacyStore.resetAll === "function")
|
|
324
471
|
return legacyStore.resetAll();
|
|
@@ -347,13 +494,20 @@ var parseOptions = (passedOptions) => {
|
|
|
347
494
|
var _a, _b, _c, _d;
|
|
348
495
|
const notUndefinedOptions = omitUndefinedOptions(passedOptions);
|
|
349
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
|
+
}
|
|
350
505
|
const config = {
|
|
351
506
|
windowMs: 60 * 1e3,
|
|
352
507
|
max: 5,
|
|
353
508
|
message: "Too many requests, please try again later.",
|
|
354
509
|
statusCode: 429,
|
|
355
|
-
legacyHeaders: (
|
|
356
|
-
standardHeaders: (_c = passedOptions.draft_polli_ratelimit_headers) != null ? _c : false,
|
|
510
|
+
legacyHeaders: (_c = passedOptions.headers) != null ? _c : true,
|
|
357
511
|
requestPropertyName: "rateLimit",
|
|
358
512
|
skipFailedRequests: false,
|
|
359
513
|
skipSuccessfulRequests: false,
|
|
@@ -372,13 +526,15 @@ var parseOptions = (passedOptions) => {
|
|
|
372
526
|
response
|
|
373
527
|
) : config.message;
|
|
374
528
|
if (!response.writableEnded) {
|
|
375
|
-
response.send(message
|
|
529
|
+
response.send(message);
|
|
376
530
|
}
|
|
377
531
|
},
|
|
378
532
|
onLimitReached(_request, _response, _optionsUsed) {
|
|
379
533
|
},
|
|
380
|
-
// Allow the options
|
|
534
|
+
// Allow the default options to be overriden by the options passed to the middleware.
|
|
381
535
|
...notUndefinedOptions,
|
|
536
|
+
// `standardHeaders` is resolved into a draft version above, use that.
|
|
537
|
+
standardHeaders,
|
|
382
538
|
// Note that this field is declared after the user's options are spread in,
|
|
383
539
|
// so that this field doesn't get overriden with an un-promisified store!
|
|
384
540
|
store: promisifyStore((_d = notUndefinedOptions.store) != null ? _d : new MemoryStore()),
|
|
@@ -414,40 +570,27 @@ var rateLimit = (passedOptions) => {
|
|
|
414
570
|
const augmentedRequest = request;
|
|
415
571
|
const key = await config.keyGenerator(request, response);
|
|
416
572
|
const { totalHits, resetTime } = await config.store.increment(key);
|
|
573
|
+
config.validations.positiveHits(totalHits);
|
|
417
574
|
config.validations.singleCount(request, config.store, key);
|
|
418
575
|
const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max;
|
|
419
576
|
const maxHits = await retrieveQuota;
|
|
420
|
-
|
|
577
|
+
config.validations.max(maxHits);
|
|
578
|
+
const info = {
|
|
421
579
|
limit: maxHits,
|
|
422
580
|
current: totalHits,
|
|
423
581
|
remaining: Math.max(maxHits - totalHits, 0),
|
|
424
582
|
resetTime
|
|
425
583
|
};
|
|
584
|
+
augmentedRequest[config.requestPropertyName] = info;
|
|
426
585
|
if (config.legacyHeaders && !response.headersSent) {
|
|
427
|
-
response
|
|
428
|
-
response.setHeader(
|
|
429
|
-
"X-RateLimit-Remaining",
|
|
430
|
-
augmentedRequest[config.requestPropertyName].remaining
|
|
431
|
-
);
|
|
432
|
-
if (resetTime instanceof Date) {
|
|
433
|
-
response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
|
|
434
|
-
response.setHeader(
|
|
435
|
-
"X-RateLimit-Reset",
|
|
436
|
-
Math.ceil(resetTime.getTime() / 1e3)
|
|
437
|
-
);
|
|
438
|
-
}
|
|
586
|
+
setLegacyHeaders(response, info);
|
|
439
587
|
}
|
|
440
588
|
if (config.standardHeaders && !response.headersSent) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (resetTime) {
|
|
447
|
-
const deltaSeconds = Math.ceil(
|
|
448
|
-
(resetTime.getTime() - Date.now()) / 1e3
|
|
449
|
-
);
|
|
450
|
-
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);
|
|
451
594
|
}
|
|
452
595
|
}
|
|
453
596
|
if (config.skipFailedRequests || config.skipSuccessfulRequests) {
|
|
@@ -483,8 +626,8 @@ var rateLimit = (passedOptions) => {
|
|
|
483
626
|
}
|
|
484
627
|
config.validations.disable();
|
|
485
628
|
if (maxHits && totalHits > maxHits) {
|
|
486
|
-
if (
|
|
487
|
-
response
|
|
629
|
+
if (config.legacyHeaders || config.standardHeaders) {
|
|
630
|
+
setRetryAfterHeader(response, info, config.windowMs);
|
|
488
631
|
}
|
|
489
632
|
config.handler(request, response, next, options);
|
|
490
633
|
return;
|
package/dist/index.d.cts
CHANGED
|
@@ -139,6 +139,7 @@ export type Store = {
|
|
|
139
139
|
*/
|
|
140
140
|
localKeys?: boolean;
|
|
141
141
|
};
|
|
142
|
+
export type DraftHeadersVersion = "draft-6" | "draft-7";
|
|
142
143
|
/**
|
|
143
144
|
* The configuration options for the rate limiter.
|
|
144
145
|
*/
|
|
@@ -183,7 +184,7 @@ export type Options = {
|
|
|
183
184
|
*
|
|
184
185
|
* Defaults to `false` (for backward compatibility, but its use is recommended).
|
|
185
186
|
*/
|
|
186
|
-
standardHeaders: boolean;
|
|
187
|
+
standardHeaders: boolean | DraftHeadersVersion;
|
|
187
188
|
/**
|
|
188
189
|
* The name of the property on the request object to store the rate limit info.
|
|
189
190
|
*
|
package/dist/index.d.mts
CHANGED
|
@@ -139,6 +139,7 @@ export type Store = {
|
|
|
139
139
|
*/
|
|
140
140
|
localKeys?: boolean;
|
|
141
141
|
};
|
|
142
|
+
export type DraftHeadersVersion = "draft-6" | "draft-7";
|
|
142
143
|
/**
|
|
143
144
|
* The configuration options for the rate limiter.
|
|
144
145
|
*/
|
|
@@ -183,7 +184,7 @@ export type Options = {
|
|
|
183
184
|
*
|
|
184
185
|
* Defaults to `false` (for backward compatibility, but its use is recommended).
|
|
185
186
|
*/
|
|
186
|
-
standardHeaders: boolean;
|
|
187
|
+
standardHeaders: boolean | DraftHeadersVersion;
|
|
187
188
|
/**
|
|
188
189
|
* The name of the property on the request object to store the rate limit info.
|
|
189
190
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -139,6 +139,7 @@ export type Store = {
|
|
|
139
139
|
*/
|
|
140
140
|
localKeys?: boolean;
|
|
141
141
|
};
|
|
142
|
+
export type DraftHeadersVersion = "draft-6" | "draft-7";
|
|
142
143
|
/**
|
|
143
144
|
* The configuration options for the rate limiter.
|
|
144
145
|
*/
|
|
@@ -183,7 +184,7 @@ export type Options = {
|
|
|
183
184
|
*
|
|
184
185
|
* Defaults to `false` (for backward compatibility, but its use is recommended).
|
|
185
186
|
*/
|
|
186
|
-
standardHeaders: boolean;
|
|
187
|
+
standardHeaders: boolean | DraftHeadersVersion;
|
|
187
188
|
/**
|
|
188
189
|
* The name of the property on the request object to store the rate limit info.
|
|
189
190
|
*
|
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,12 +65,12 @@ 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
72
|
const url = `https://express-rate-limit.github.io/${code}/`;
|
|
20
|
-
super(`${message} See ${url} for more information
|
|
73
|
+
super(`${message} See ${url} for more information.`);
|
|
21
74
|
__publicField(this, "name");
|
|
22
75
|
__publicField(this, "code");
|
|
23
76
|
__publicField(this, "help");
|
|
@@ -26,6 +79,8 @@ var ValidationError = class extends Error {
|
|
|
26
79
|
this.help = url;
|
|
27
80
|
}
|
|
28
81
|
};
|
|
82
|
+
var ChangeWarning = class extends ValidationError {
|
|
83
|
+
};
|
|
29
84
|
var _Validations = class _Validations {
|
|
30
85
|
constructor(enabled) {
|
|
31
86
|
// eslint-disable-next-line @typescript-eslint/parameter-properties
|
|
@@ -103,6 +158,22 @@ var _Validations = class _Validations {
|
|
|
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
|
+
}
|
|
106
177
|
/**
|
|
107
178
|
* Ensures a given key is incremented only once per request.
|
|
108
179
|
*
|
|
@@ -134,6 +205,78 @@ var _Validations = class _Validations {
|
|
|
134
205
|
keys.push(key);
|
|
135
206
|
});
|
|
136
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
|
+
}
|
|
137
280
|
wrap(validation) {
|
|
138
281
|
if (!this.enabled) {
|
|
139
282
|
return;
|
|
@@ -141,7 +284,10 @@ var _Validations = class _Validations {
|
|
|
141
284
|
try {
|
|
142
285
|
validation.call(this);
|
|
143
286
|
} catch (error) {
|
|
144
|
-
|
|
287
|
+
if (error instanceof ChangeWarning)
|
|
288
|
+
console.warn(error);
|
|
289
|
+
else
|
|
290
|
+
console.error(error);
|
|
145
291
|
}
|
|
146
292
|
}
|
|
147
293
|
};
|
|
@@ -293,6 +439,7 @@ var promisifyStore = (passedStore) => {
|
|
|
293
439
|
async resetKey(key) {
|
|
294
440
|
return legacyStore.resetKey(key);
|
|
295
441
|
}
|
|
442
|
+
/* istanbul ignore next */
|
|
296
443
|
async resetAll() {
|
|
297
444
|
if (typeof legacyStore.resetAll === "function")
|
|
298
445
|
return legacyStore.resetAll();
|
|
@@ -321,13 +468,20 @@ var parseOptions = (passedOptions) => {
|
|
|
321
468
|
var _a, _b, _c, _d;
|
|
322
469
|
const notUndefinedOptions = omitUndefinedOptions(passedOptions);
|
|
323
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
|
+
}
|
|
324
479
|
const config = {
|
|
325
480
|
windowMs: 60 * 1e3,
|
|
326
481
|
max: 5,
|
|
327
482
|
message: "Too many requests, please try again later.",
|
|
328
483
|
statusCode: 429,
|
|
329
|
-
legacyHeaders: (
|
|
330
|
-
standardHeaders: (_c = passedOptions.draft_polli_ratelimit_headers) != null ? _c : false,
|
|
484
|
+
legacyHeaders: (_c = passedOptions.headers) != null ? _c : true,
|
|
331
485
|
requestPropertyName: "rateLimit",
|
|
332
486
|
skipFailedRequests: false,
|
|
333
487
|
skipSuccessfulRequests: false,
|
|
@@ -346,13 +500,15 @@ var parseOptions = (passedOptions) => {
|
|
|
346
500
|
response
|
|
347
501
|
) : config.message;
|
|
348
502
|
if (!response.writableEnded) {
|
|
349
|
-
response.send(message
|
|
503
|
+
response.send(message);
|
|
350
504
|
}
|
|
351
505
|
},
|
|
352
506
|
onLimitReached(_request, _response, _optionsUsed) {
|
|
353
507
|
},
|
|
354
|
-
// Allow the options
|
|
508
|
+
// Allow the default options to be overriden by the options passed to the middleware.
|
|
355
509
|
...notUndefinedOptions,
|
|
510
|
+
// `standardHeaders` is resolved into a draft version above, use that.
|
|
511
|
+
standardHeaders,
|
|
356
512
|
// Note that this field is declared after the user's options are spread in,
|
|
357
513
|
// so that this field doesn't get overriden with an un-promisified store!
|
|
358
514
|
store: promisifyStore((_d = notUndefinedOptions.store) != null ? _d : new MemoryStore()),
|
|
@@ -388,40 +544,27 @@ var rateLimit = (passedOptions) => {
|
|
|
388
544
|
const augmentedRequest = request;
|
|
389
545
|
const key = await config.keyGenerator(request, response);
|
|
390
546
|
const { totalHits, resetTime } = await config.store.increment(key);
|
|
547
|
+
config.validations.positiveHits(totalHits);
|
|
391
548
|
config.validations.singleCount(request, config.store, key);
|
|
392
549
|
const retrieveQuota = typeof config.max === "function" ? config.max(request, response) : config.max;
|
|
393
550
|
const maxHits = await retrieveQuota;
|
|
394
|
-
|
|
551
|
+
config.validations.max(maxHits);
|
|
552
|
+
const info = {
|
|
395
553
|
limit: maxHits,
|
|
396
554
|
current: totalHits,
|
|
397
555
|
remaining: Math.max(maxHits - totalHits, 0),
|
|
398
556
|
resetTime
|
|
399
557
|
};
|
|
558
|
+
augmentedRequest[config.requestPropertyName] = info;
|
|
400
559
|
if (config.legacyHeaders && !response.headersSent) {
|
|
401
|
-
response
|
|
402
|
-
response.setHeader(
|
|
403
|
-
"X-RateLimit-Remaining",
|
|
404
|
-
augmentedRequest[config.requestPropertyName].remaining
|
|
405
|
-
);
|
|
406
|
-
if (resetTime instanceof Date) {
|
|
407
|
-
response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
|
|
408
|
-
response.setHeader(
|
|
409
|
-
"X-RateLimit-Reset",
|
|
410
|
-
Math.ceil(resetTime.getTime() / 1e3)
|
|
411
|
-
);
|
|
412
|
-
}
|
|
560
|
+
setLegacyHeaders(response, info);
|
|
413
561
|
}
|
|
414
562
|
if (config.standardHeaders && !response.headersSent) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (resetTime) {
|
|
421
|
-
const deltaSeconds = Math.ceil(
|
|
422
|
-
(resetTime.getTime() - Date.now()) / 1e3
|
|
423
|
-
);
|
|
424
|
-
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);
|
|
425
568
|
}
|
|
426
569
|
}
|
|
427
570
|
if (config.skipFailedRequests || config.skipSuccessfulRequests) {
|
|
@@ -457,8 +600,8 @@ var rateLimit = (passedOptions) => {
|
|
|
457
600
|
}
|
|
458
601
|
config.validations.disable();
|
|
459
602
|
if (maxHits && totalHits > maxHits) {
|
|
460
|
-
if (
|
|
461
|
-
response
|
|
603
|
+
if (config.legacyHeaders || config.standardHeaders) {
|
|
604
|
+
setRetryAfterHeader(response, info, config.windowMs);
|
|
462
605
|
}
|
|
463
606
|
config.handler(request, response, next, options);
|
|
464
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",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"format:code": "npm run lint:code -- --fix",
|
|
67
67
|
"format:rest": "npm run lint:rest -- --write .",
|
|
68
68
|
"format": "run-s format:*",
|
|
69
|
-
"test:lib": "cross-env NODE_NO_WARNINGS=1 NODE_OPTIONS=--experimental-vm-modules jest",
|
|
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
71
|
"test": "run-s lint test:lib",
|
|
72
72
|
"pre-commit": "lint-staged",
|
|
@@ -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,7 +12,7 @@ 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
|
|
|
@@ -97,13 +97,13 @@ Import it in a CommonJS project (`type: commonjs` or no `type` field in
|
|
|
97
97
|
`package.json`) as follows:
|
|
98
98
|
|
|
99
99
|
```ts
|
|
100
|
-
const rateLimit = require('express-rate-limit')
|
|
100
|
+
const { rateLimit } = require('express-rate-limit')
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
Import it in a ESM project (`type: module` in `package.json`) as follows:
|
|
104
104
|
|
|
105
105
|
```ts
|
|
106
|
-
import rateLimit from 'express-rate-limit'
|
|
106
|
+
import { rateLimit } from 'express-rate-limit'
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
### Examples
|
|
@@ -112,13 +112,13 @@ To use it in an API-only server where the rate-limiter should be applied to all
|
|
|
112
112
|
requests:
|
|
113
113
|
|
|
114
114
|
```ts
|
|
115
|
-
import rateLimit from 'express-rate-limit'
|
|
115
|
+
import { rateLimit } from 'express-rate-limit'
|
|
116
116
|
|
|
117
117
|
const limiter = rateLimit({
|
|
118
118
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
119
119
|
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
120
|
-
standardHeaders:
|
|
121
|
-
legacyHeaders: false, //
|
|
120
|
+
standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
|
|
121
|
+
legacyHeaders: false, // X-RateLimit-* headers
|
|
122
122
|
// store: ... , // Use an external store for more precise rate limiting
|
|
123
123
|
})
|
|
124
124
|
|
|
@@ -131,12 +131,12 @@ To use it in a 'regular' web server (e.g. anything that uses
|
|
|
131
131
|
requests:
|
|
132
132
|
|
|
133
133
|
```ts
|
|
134
|
-
import rateLimit from 'express-rate-limit'
|
|
134
|
+
import { rateLimit } from 'express-rate-limit'
|
|
135
135
|
|
|
136
136
|
const apiLimiter = rateLimit({
|
|
137
137
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
138
138
|
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
139
|
-
standardHeaders:
|
|
139
|
+
standardHeaders: 'draft-7', // Set `RateLimit` and `RateLimit-Policy`` headers
|
|
140
140
|
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
|
141
141
|
// store: ... , // Use an external store for more precise rate limiting
|
|
142
142
|
})
|
|
@@ -148,13 +148,13 @@ app.use('/api', apiLimiter)
|
|
|
148
148
|
To create multiple instances to apply different rules to different endpoints:
|
|
149
149
|
|
|
150
150
|
```ts
|
|
151
|
-
import rateLimit from 'express-rate-limit'
|
|
151
|
+
import { rateLimit } from 'express-rate-limit'
|
|
152
152
|
|
|
153
153
|
const apiLimiter = rateLimit({
|
|
154
154
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
155
155
|
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
156
|
-
standardHeaders:
|
|
157
|
-
legacyHeaders: false, //
|
|
156
|
+
standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
|
|
157
|
+
legacyHeaders: false, // X-RateLimit-* headers
|
|
158
158
|
// store: ... , // Use an external store for more precise rate limiting
|
|
159
159
|
})
|
|
160
160
|
|
|
@@ -165,8 +165,8 @@ const createAccountLimiter = rateLimit({
|
|
|
165
165
|
max: 5, // Limit each IP to 5 create account requests per `window` (here, per hour)
|
|
166
166
|
message:
|
|
167
167
|
'Too many accounts created from this IP, please try again after an hour',
|
|
168
|
-
standardHeaders:
|
|
169
|
-
legacyHeaders: false, //
|
|
168
|
+
standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
|
|
169
|
+
legacyHeaders: false, // X-RateLimit-* headers
|
|
170
170
|
})
|
|
171
171
|
|
|
172
172
|
app.post('/create-account', createAccountLimiter, (request, response) => {
|
|
@@ -177,7 +177,7 @@ app.post('/create-account', createAccountLimiter, (request, response) => {
|
|
|
177
177
|
To use a custom store:
|
|
178
178
|
|
|
179
179
|
```ts
|
|
180
|
-
import rateLimit from 'express-rate-limit'
|
|
180
|
+
import { rateLimit } from 'express-rate-limit'
|
|
181
181
|
import RedisStore from 'rate-limit-redis'
|
|
182
182
|
import RedisClient from 'ioredis'
|
|
183
183
|
|
|
@@ -185,7 +185,8 @@ const redisClient = new RedisClient()
|
|
|
185
185
|
const rateLimiter = rateLimit({
|
|
186
186
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
187
187
|
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
|
188
|
-
standardHeaders:
|
|
188
|
+
standardHeaders: 'draft-7', // draft-6: RateLimit-* headers; draft-7: combined RateLimit header
|
|
189
|
+
legacyHeaders: false, // X-RateLimit-* headers
|
|
189
190
|
store: new RedisStore({
|
|
190
191
|
/* ... */
|
|
191
192
|
}), // Use the external store
|
|
@@ -329,16 +330,38 @@ Defaults to `true` (for backward compatibility).
|
|
|
329
330
|
|
|
330
331
|
### `standardHeaders`
|
|
331
332
|
|
|
332
|
-
> `boolean`
|
|
333
|
+
> `boolean` | `'draft-6'` | `'draft-7'`
|
|
333
334
|
|
|
334
335
|
Whether to enable support for headers conforming to the
|
|
335
|
-
[
|
|
336
|
-
adopted by the IETF
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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.
|
|
340
350
|
|
|
341
|
-
|
|
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`.
|
|
342
365
|
|
|
343
366
|
> Renamed in `6.x` from `draft_polli_ratelimit_headers` to `standardHeaders`.
|
|
344
367
|
|
|
@@ -430,8 +453,8 @@ const limiter = rateLimit({
|
|
|
430
453
|
> `function`
|
|
431
454
|
|
|
432
455
|
A (sync/async) function that accepts the Express `request` and `response`
|
|
433
|
-
objects that is called
|
|
434
|
-
rate
|
|
456
|
+
objects that is called the on the request where a client has just exceeded their
|
|
457
|
+
rate limit.
|
|
435
458
|
|
|
436
459
|
This method was
|
|
437
460
|
[deprecated in v6](https://github.com/express-rate-limit/express-rate-limit/releases/v6.0.0) -
|
|
@@ -515,7 +538,7 @@ Here is a list of external stores:
|
|
|
515
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 |
|
|
516
539
|
| [`rate-limit-memcached`](https://npmjs.org/package/rate-limit-memcached) | A [Memcached](https://memcached.org/)-backed store. | Legacy |
|
|
517
540
|
| [`rate-limit-mongo`](https://www.npmjs.com/package/rate-limit-mongo) | A [MongoDB](https://www.mongodb.com/)-backed store. | Legacy |
|
|
518
|
-
| [`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 |
|
|
519
542
|
|
|
520
543
|
Take a look at
|
|
521
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
|
}
|