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 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 a lowercase character,
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
- super(
46
- `express-rate-limit: ${code} - ${message} See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#${code.toLowerCase()} for more information on this error.`
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.message = message;
105
+ this.help = url;
53
106
  }
54
107
  };
55
- var Validations = class {
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
- console.error(error);
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: (_b = passedOptions.headers) != null ? _b : true,
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 != null ? message : "Too many requests, please try again later.");
529
+ response.send(message);
328
530
  }
329
531
  },
330
532
  onLimitReached(_request, _response, _optionsUsed) {
331
533
  },
332
- // Allow the options object to be overriden by the options passed to the middleware.
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
- augmentedRequest[config.requestPropertyName] = {
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.setHeader("X-RateLimit-Limit", maxHits);
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
- response.setHeader("RateLimit-Limit", maxHits);
393
- response.setHeader(
394
- "RateLimit-Remaining",
395
- augmentedRequest[config.requestPropertyName].remaining
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 ((config.legacyHeaders || config.standardHeaders) && !response.headersSent) {
438
- response.setHeader("Retry-After", Math.ceil(config.windowMs / 1e3));
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 a lowercase character,
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
- super(
20
- `express-rate-limit: ${code} - ${message} See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#${code.toLowerCase()} for more information on this error.`
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.message = message;
79
+ this.help = url;
27
80
  }
28
81
  };
29
- var Validations = class {
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
- console.error(error);
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: (_b = passedOptions.headers) != null ? _b : true,
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 != null ? message : "Too many requests, please try again later.");
503
+ response.send(message);
302
504
  }
303
505
  },
304
506
  onLimitReached(_request, _response, _optionsUsed) {
305
507
  },
306
- // Allow the options object to be overriden by the options passed to the middleware.
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
- augmentedRequest[config.requestPropertyName] = {
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.setHeader("X-RateLimit-Limit", maxHits);
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
- response.setHeader("RateLimit-Limit", maxHits);
367
- response.setHeader(
368
- "RateLimit-Remaining",
369
- augmentedRequest[config.requestPropertyName].remaining
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 ((config.legacyHeaders || config.standardHeaders) && !response.headersSent) {
412
- response.setHeader("Retry-After", Math.ceil(config.windowMs / 1e3));
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.8.1",
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.0.0"
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
- "autofix:code": "npm run lint:code -- --fix",
67
- "autofix:rest": "npm run lint:rest -- --write .",
68
- "autofix": "run-s autofix:*",
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
- "@jest/globals": "29.6.1",
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.2",
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.1",
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
- [![Tests](https://github.com/express-rate-limit/express-rate-limit/workflows/Test/badge.svg)](https://github.com/express-rate-limit/express-rate-limit/actions)
15
+ [![tests](https://github.com/express-rate-limit/express-rate-limit/actions/workflows/ci.yaml/badge.svg)](https://github.com/express-rate-limit/express-rate-limit/actions/workflows/ci.yaml)
16
16
  [![npm version](https://img.shields.io/npm/v/express-rate-limit.svg)](https://npmjs.org/package/express-rate-limit 'View this project on NPM')
17
17
  [![npm downloads](https://img.shields.io/npm/dm/express-rate-limit)](https://www.npmjs.com/package/express-rate-limit)
18
18
 
19
- Basic rate-limiting middleware for Express. Use to limit repeated requests to
20
- public APIs and/or endpoints such as password reset. Plays nice with
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
- ### Alternate Rate Limiters
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
- > This module does not share state with other processes/servers by default. If
28
- > you need a more robust solution, I recommend using an external store. See the
29
- > [`stores` section](#store) below for a list of external stores.
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: true, // Return rate limit info in the `RateLimit-*` headers
96
- legacyHeaders: false, // Disable the `X-RateLimit-*` headers
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: true, // Return rate limit info in the `RateLimit-*` headers
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: true, // Return rate limit info in the `RateLimit-*` headers
130
- legacyHeaders: false, // Disable the `X-RateLimit-*` headers
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: true, // Return rate limit info in the `RateLimit-*` headers
141
- legacyHeaders: false, // Disable the `X-RateLimit-*` headers
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 rateLimit, { MemoryStore } from 'express-rate-limit'
180
+ import { rateLimit } from 'express-rate-limit'
181
+ import RedisStore from 'rate-limit-redis'
182
+ import RedisClient from 'ioredis'
153
183
 
154
- const apiLimiter = rateLimit({
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: true, // Return rate limit info in the `RateLimit-*` headers
158
- store: new MemoryStore(),
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 API calls only
162
- app.use('/api', apiLimiter)
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
- [ratelimit standardization draft](https://github.com/ietf-wg-httpapi/ratelimit-headers/blob/main/draft-ietf-httpapi-ratelimit-headers.md)
303
- adopted by the IETF (`RateLimit-Limit`, `RateLimit-Remaining`, and, if the store
304
- supports it, `RateLimit-Reset`). If set to `true`, the middleware also sends the
305
- `Retry-After` header on all blocked requests. May be used in conjunction with,
306
- or instead of the `legacyHeaders` option.
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
- Defaults to `false` (for backward compatibility, but its use is recommended).
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 when a client has reached their rate limit, and will be
401
- rate limited on their next request.
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. | Legacy |
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
- "compilerOptions": {
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
  }