Skip to content

Commit

Permalink
feat: storePerLimiter validation check (#459)
Browse files Browse the repository at this point in the history
* add new storePerLimiter validation check
Checks for cases like #457 where a single store instance is shared across multiple rate limiter instances.

* automatic formatting change

* npm audit fix dep bump

* rename validation to singleCount
  • Loading branch information
nfriedly committed Jun 1, 2024
1 parent 7c4d1c6 commit e58b370
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 9 deletions.
7 changes: 7 additions & 0 deletions docs/reference/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.3.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v7.2.1)

### Added

- New `unsharedStore` validation check that identifies cases where a single
store instance is shared across multiple limiters

## [7.2.0](https://github.com/express-rate-limit/express-rate-limit/releases/tag/v7.2.0)

### Added
Expand Down
1 change: 1 addition & 0 deletions docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ Supported validations are:
- [trustProxy](/reference/error-codes#err-erl-permissive-trust-proxy)
- [xForwardedForHeader](/reference/error-codes#err-erl-unexpected-x-forwarded-for)
- [positiveHits](/reference/error-codes#err-erl-invalid-hits)
- [unsharedStore](/reference/error-codes#err-erl-store-reuse)
- [singleCount](/reference/error-codes#err-erl-double-count)
- [limit](/reference/error-codes#wrn-erl-max-zero) (Note: the `limit` option was
renamed to `max`, but the validation name did not change.)
Expand Down
13 changes: 13 additions & 0 deletions docs/reference/error-codes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ error.

Set `validate: {positiveHits: false}` in the options to disable the check.

### `ERR_ERL_STORE_REUSE`

> Added in `7.3.0`.
This indicates that the single [Store](./stores) instance was used in more than
one rate limiter. This can lead to problems such as initialization logic runing
multiple times, and inconsistent reset times.

Instead, create a new store instance for each rate limiter. (Generally with a
unique `prefix` value for each one.)

Set `validate: {unsharedStore: false}` in the options to disable the check.

### `ERR_ERL_DOUBLE_COUNT`

> Added in `6.9.0`.
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions source/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,10 @@ const rateLimit = (
const config = parseOptions(passedOptions ?? {})
const options = getOptionsFromConfig(config)

// The limiter shouldn't be created in response to a request
config.validations.creationStack()
// The store instance shouldn't be shared across multiple limiters
config.validations.unsharedStore(config.store)

// Call the `init` method on the store, if it exists
if (typeof config.store.init === 'function') config.store.init(options)
Expand Down
22 changes: 22 additions & 0 deletions source/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class ValidationError extends Error {
*/
class ChangeWarning extends ValidationError {}

/**
* List of store instances that have been used with any express-rate-limit instance
*/
const usedStores = new Set<Store>()

/**
* Maps the key used in a store for a certain request, and ensures that the
* same key isn't used more than once per request.
Expand Down Expand Up @@ -142,6 +147,23 @@ const validations = {
}
},

/**
* Ensures a single store instance is not used with multiple express-rate-limit instances
*/
unsharedStore(store: Store) {
if (usedStores.has(store)) {
const maybeUniquePrefix = store?.localKeys
? ''
: ' (with a unique prefix)'
throw new ValidationError(
'ERR_ERL_STORE_REUSE',
`A Store instance must not be shared across multiple rate limiters. Create a new instance of ${store.constructor.name}${maybeUniquePrefix} for each limiter instead.`,
)
}

usedStores.add(store)
},

/**
* Ensures a given key is incremented only once per request.
*
Expand Down
4 changes: 1 addition & 3 deletions test/library/middleware-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,7 @@ describe('middleware test', () => {
.expect((response) => {
if ('retry-after' in response.headers) {
throw new Error(
`Expected no retry-after header, got ${
response.headers['retry-after'] as string
}`,
`Expected no retry-after header, got ${response.headers['retry-after']}`,
)
}
})
Expand Down
24 changes: 24 additions & 0 deletions test/library/validation-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,30 @@ describe('validations tests', () => {
})
})

describe('unsharedStore', () => {
it('should log an error if a store instance is used in two limiters', () => {
const store = { localKeys: true }

validations.unsharedStore(store as Store)
expect(console.error).not.toBeCalled()
validations.unsharedStore(store as Store)
expect(console.error).toHaveBeenCalledWith(
expect.objectContaining({
code: 'ERR_ERL_STORE_REUSE',
}),
)
})

it('should not log an error if multiple store instances are used', () => {
const store1 = { localKeys: true }
const store2 = { localKeys: true }

validations.unsharedStore(store1 as Store)
validations.unsharedStore(store2 as Store)
expect(console.error).not.toBeCalled()
})
})

describe('singleCount', () => {
class TestExternalStore {
prefix?: string
Expand Down

0 comments on commit e58b370

Please sign in to comment.