├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .npmrc ├── .prettierignore ├── changelog.md ├── config └── husky │ └── pre-commit ├── jest.config.json ├── license.md ├── package-lock.json ├── package.json ├── readme.md ├── source ├── index.ts ├── slow-down.ts └── types.ts ├── test ├── helpers │ ├── mock-stores.ts │ ├── requests.ts │ └── server.ts ├── integration │ └── integration-test.ts └── library │ ├── connection-test.ts │ ├── delay-test.ts │ ├── instance-api-test.ts │ ├── middleware-test.ts │ ├── options-test.ts │ └── store-test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # /.editorconfig 2 | # Tells most editors what our style preferences are 3 | # https://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | max_line_length = 80 13 | indent_size = tab 14 | tab_width = 2 15 | 16 | [*.{ts,json,md}] 17 | indent_style = tab 18 | 19 | [*.yaml] 20 | indent_style = spaces 21 | indent_size = 2 22 | 23 | [package*.json] 24 | indent_style = space 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # /.gitattributes 2 | # Makes sure all line endings are LF 3 | 4 | * text=auto eol=lf 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: Lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout the repository 11 | uses: actions/checkout@v4 12 | - name: Use Node ${{ matrix.node-version }} 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: lts/* 16 | - name: Check for lint/formatting errors 17 | run: | 18 | npm ci 19 | npm run lint 20 | test-library: 21 | name: Test (Library) 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | node-version: [lts/*, latest] 26 | os: [ubuntu-latest, windows-latest, macos-latest] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Checkout the repository 30 | uses: actions/checkout@v4 31 | - name: Use Node ${{ matrix.node-version }} 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | - name: Run library tests 36 | run: | 37 | npm ci 38 | npm run test:lib 39 | test-integration: 40 | name: Test (Integration) 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | node-version: [lts/*, latest] 45 | os: [ubuntu-latest] 46 | runs-on: ${{ matrix.os }} 47 | steps: 48 | - name: Checkout the repository 49 | uses: actions/checkout@v4 50 | - name: Use Node ${{ matrix.node-version }} 51 | uses: actions/setup-node@v3 52 | with: 53 | node-version: ${{ matrix.node-version }} 54 | - name: Run integration tests 55 | run: | 56 | npm ci 57 | npm run test:int 58 | publish: 59 | name: Publish 60 | needs: [lint, test-library, test-integration] 61 | if: startsWith(github.ref, 'refs/tags/v') 62 | runs-on: ubuntu-latest 63 | permissions: 64 | id-token: write 65 | steps: 66 | - name: Checkout the repository 67 | uses: actions/checkout@v4 68 | - uses: actions/setup-node@v3 69 | with: 70 | node-version: lts/* 71 | registry-url: https://registry.npmjs.org/ 72 | - name: Install dependencies 73 | run: npm ci 74 | - name: Publish package to NPM 75 | run: npm publish --provenance 76 | env: 77 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # /.gitignore 2 | # Tells Git to ignore these files 3 | 4 | node_modules/ 5 | dist/ 6 | coverage/ 7 | 8 | .vscode/ 9 | .idea/ 10 | 11 | *.log 12 | *.tmp 13 | *.bak 14 | *.tgz 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # .npmrc 2 | # Configuration for npm and pnpm 3 | 4 | # Uses the exact version instead of any within-patch-range version of an 5 | # installed package 6 | save-exact=true 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # /.prettierignore 2 | # Tells Prettier to ignore these files 3 | 4 | package.json 5 | package-lock.json 6 | pnpm-lock.yaml 7 | 8 | # The following is copied over from .gitignore 9 | 10 | node_modules/ 11 | dist/ 12 | coverage/ 13 | 14 | .vscode/ 15 | .idea/ 16 | 17 | *.log 18 | *.tmp 19 | *.bak 20 | *.tgz 21 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # express-slow-down changelog 2 | 3 | ## v2.1.0 4 | 5 | - Changed distributed JS to no longer bundle in `express-rate-limit`, instead 6 | using the version installed via npm. This enables several new 7 | express-rate-limit features that have been released since v7.0.1. 8 | 9 | ## v2.0.3 10 | 11 | ### Fixed 12 | 13 | - Fixed `peerDependencies` compatibility with express 5 beta. 14 | 15 | ## v2.0.2 16 | 17 | ### Fixed 18 | 19 | - Allowed `express-slow-down` to be used with `express` v5. 20 | 21 | ## v2.0.1 22 | 23 | ### Fixed 24 | 25 | - Fixed an incorrect `WRN_ERL_MAX_ZERO` warning when supplying a custom 26 | validation object in the config. 27 | 28 | ## v2.0.0 29 | 30 | express-slow-down v2 is built on top of express-rate-limit v7. 31 | 32 | ### Breaking 33 | 34 | - Changed behavior of `delayMs` when set to a number 35 | - Previous behavior multiplied `delayMs` value by the number of slowed 36 | requests to determine the delay amount 37 | - New behavior treats a numeric value as a fixed delay that is applied to each 38 | slowed request without multiplication 39 | - Set to `function(used) { return (used - this.delayAfter) * 1000; }` to 40 | restore old behavior. (Change `1000` to match old value if necessary.) 41 | - Changed arguments passed to `delayMs` when set to a function 42 | - Previous signature was `function(req, res): number` 43 | - New signature is `function(used, req, res): number | Promise` where 44 | `used` is the number of hits from this user during the current window 45 | - Dropped support for `onLimitReached` method 46 | - Dropped support for `headers` option 47 | - Renamed `req.slowDown.current` to `req.slowDown.used` 48 | - `current` is now a hidden getter that will return the `used` value, but will 49 | not be included when iteration over keys or running through 50 | `JSON.stringify()` 51 | 52 | ### Added 53 | 54 | - `delayAfter`, `delayMs`, and `maxDelayMs` may now be async functions that 55 | return a number or a promise that resolves to a number 56 | - The MemoryStore now uses precise, per-user reset times rather than a global 57 | window that resets all users at once. 58 | - Now using express-rate-limit's validator to detect and warn about common 59 | misconfigurations. See 60 | https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes for 61 | more info. 62 | -------------------------------------------------------------------------------- /config/husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run pre-commit 5 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest/presets/default-esm", 3 | "collectCoverage": true, 4 | "collectCoverageFrom": ["source/**/*.ts"], 5 | "testTimeout": 30000, 6 | "testMatch": ["**/test/{library,integration}/*-test.[jt]s?(x)"], 7 | "moduleFileExtensions": ["js", "jsx", "json", "ts", "tsx"], 8 | "moduleNameMapper": { 9 | "^(\\.{1,2}/.*)\\.js$": "$1" 10 | }, 11 | "setupFilesAfterEnv": ["jest-expect-message"] 12 | } 13 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright 2023 Nathan Friedly, Vedant K 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-slow-down", 3 | "version": "2.1.0", 4 | "description": "Basic IP rate-limiting middleware for Express that slows down responses rather than blocking the user.", 5 | "homepage": "https://github.com/express-rate-limit/express-slow-down", 6 | "author": { 7 | "name": "Nathan Friedly", 8 | "url": "http://nfriedly.com/" 9 | }, 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/express-rate-limit/express-slow-down.git" 14 | }, 15 | "keywords": [ 16 | "express-rate-limit", 17 | "express", 18 | "rate", 19 | "limit", 20 | "ratelimit", 21 | "rate-limit", 22 | "middleware", 23 | "ip", 24 | "auth", 25 | "authorization", 26 | "security", 27 | "brute", 28 | "force", 29 | "bruteforce", 30 | "brute-force", 31 | "attack" 32 | ], 33 | "type": "module", 34 | "exports": { 35 | ".": { 36 | "import": { 37 | "types": "./dist/index.d.mts", 38 | "default": "./dist/index.mjs" 39 | }, 40 | "require": { 41 | "types": "./dist/index.d.cts", 42 | "default": "./dist/index.cjs" 43 | } 44 | } 45 | }, 46 | "main": "dist/index.cjs", 47 | "module": "./dist/index.mjs", 48 | "types": "./dist/index.d.ts", 49 | "files": [ 50 | "dist/", 51 | "tsconfig.json", 52 | "readme.md", 53 | "license.md", 54 | "changelog.md" 55 | ], 56 | "engines": { 57 | "node": ">= 16" 58 | }, 59 | "scripts": { 60 | "clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz", 61 | "build:cjs": "esbuild --platform=node --bundle --target=es2022 --packages=external --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = slowDown; module.exports.default = slowDown; module.exports.slowDown = slowDown; \" source/index.ts", 62 | "build:esm": "esbuild --platform=node --bundle --target=es2022 --packages=external --format=esm --outfile=dist/index.mjs source/index.ts", 63 | "build:types": "dts-bundle-generator --out-file=dist/index.d.ts source/index.ts && cp dist/index.d.ts dist/index.d.cts && cp dist/index.d.ts dist/index.d.mts", 64 | "compile": "run-s clean build:*", 65 | "lint:code": "xo", 66 | "lint:rest": "prettier --check .", 67 | "lint": "run-s lint:*", 68 | "format:code": "xo --fix", 69 | "format:rest": "prettier --write .", 70 | "format": "run-s format:*", 71 | "test:lib": "jest test/library/", 72 | "test:int": "jest test/integration/", 73 | "test": "run-s lint test:*", 74 | "pre-commit": "lint-staged", 75 | "prepare": "run-s compile && husky install config/husky" 76 | }, 77 | "peerDependencies": { 78 | "express": "4 || 5 || ^5.0.0-beta.1" 79 | }, 80 | "dependencies": { 81 | "express-rate-limit": "7" 82 | }, 83 | "devDependencies": { 84 | "@express-rate-limit/prettier": "1.1.1", 85 | "@express-rate-limit/tsconfig": "1.0.2", 86 | "@jest/globals": "29.7.0", 87 | "@types/express": "4.17.18", 88 | "@types/jest": "29.5.5", 89 | "@types/supertest": "2.0.12", 90 | "body-parser": "1.20.3", 91 | "del-cli": "5.1.0", 92 | "dts-bundle-generator": "8.0.1", 93 | "esbuild": "0.25.0", 94 | "express": "4.21.2", 95 | "husky": "8.0.3", 96 | "jest": "29.7.0", 97 | "jest-expect-message": "1.1.3", 98 | "lint-staged": "14.0.1", 99 | "npm-run-all": "4.1.5", 100 | "prettier": "3.0.3", 101 | "supertest": "6.3.3", 102 | "ts-jest": "29.1.1", 103 | "typescript": "5.2.2", 104 | "xo": "0.56.0" 105 | }, 106 | "xo": { 107 | "prettier": true, 108 | "rules": { 109 | "@typescript-eslint/prefer-nullish-coalescing": [ 110 | "error", 111 | { 112 | "ignoreConditionalTests": true 113 | } 114 | ], 115 | "@typescript-eslint/no-confusing-void-expression": 0, 116 | "@typescript-eslint/consistent-indexed-object-style": [ 117 | "error", 118 | "index-signature" 119 | ], 120 | "unicorn/prefer-string-replace-all": 0 121 | }, 122 | "overrides": [ 123 | { 124 | "files": "test/**/*.ts", 125 | "rules": { 126 | "@typescript-eslint/no-unsafe-argument": 0, 127 | "@typescript-eslint/no-unsafe-assignment": 0, 128 | "@typescript-eslint/no-unsafe-call": 0, 129 | "@typescript-eslint/no-unsafe-return": 0, 130 | "@typescript-eslint/no-empty-function": 0, 131 | "import/no-named-as-default": 0, 132 | "unicorn/prefer-event-target": 0, 133 | "unicorn/prevent-abbreviations": 0 134 | } 135 | } 136 | ] 137 | }, 138 | "prettier": "@express-rate-limit/prettier", 139 | "lint-staged": { 140 | "{source,test}/**/*.ts": "xo --fix", 141 | "**/*.{json,yaml,md}": "prettier --write" 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | #
Express Slow Down
2 | 3 |
4 | 5 | [![tests](https://github.com/express-rate-limit/express-slow-down/actions/workflows/ci.yaml/badge.svg)](https://github.com/express-rate-limit/express-slow-down/actions/workflows/ci.yaml) 6 | [![npm version](https://img.shields.io/npm/v/express-slow-down.svg)](https://npmjs.org/package/express-slow-down 'View this project on NPM') 7 | [![npm downloads](https://img.shields.io/npm/dm/express-slow-down)](https://www.npmjs.com/package/express-slow-down) 8 | 9 | Basic rate-limiting middleware for Express that slows down responses rather than 10 | blocking them outright. Use to slow repeated requests to public APIs and/or 11 | endpoints such as password reset. 12 | 13 | Plays nice with (and built on top of) 14 | [Express Rate Limit](https://npmjs.org/package/express-rate-limit) 15 | 16 |
17 | 18 | ### Stores 19 | 20 | The default memory store does not share state with any other processes or 21 | servers. It's sufficient for basic abuse prevention, but an external store will 22 | provide more consistency. 23 | 24 | express-slow-down uses 25 | [express-rate-limit's stores](https://express-rate-limit.mintlify.app/reference/stores) 26 | 27 | > **Note**: when using express-slow-down and express-rate-limit with an external 28 | > store, you'll need to create two instances of the store and provide different 29 | > prefixes so that they don't double-count requests. 30 | 31 | ## Installation 32 | 33 | From the npm registry: 34 | 35 | ```sh 36 | # Using npm 37 | > npm install express-slow-down 38 | # Using yarn or pnpm 39 | > yarn/pnpm add express-slow-down 40 | ``` 41 | 42 | From Github Releases: 43 | 44 | ```sh 45 | # Using npm 46 | > npm install https://github.com/express-rate-limit/express-slow-down/releases/download/v{version}/express-slow-down.tgz 47 | # Using yarn or pnpm 48 | > yarn/pnpm add https://github.com/express-rate-limit/express-slow-down/releases/download/v{version}/express-slow-down.tgz 49 | ``` 50 | 51 | Replace `{version}` with the version of the package that you want to your, e.g.: 52 | `2.0.0`. 53 | 54 | ## Usage 55 | 56 | ### Importing 57 | 58 | This library is provided in ESM as well as CJS forms, and works with both 59 | Javascript and Typescript projects. 60 | 61 | **This package requires you to use Node 16 or above.** 62 | 63 | Import it in a CommonJS project (`type: commonjs` or no `type` field in 64 | `package.json`) as follows: 65 | 66 | ```ts 67 | const { slowDown } = require('express-slow-down') 68 | ``` 69 | 70 | Import it in a ESM project (`type: module` in `package.json`) as follows: 71 | 72 | ```ts 73 | import { slowDown } from 'express-slow-down' 74 | ``` 75 | 76 | ### Examples 77 | 78 | To use it in an API-only server where the speed-limiter should be applied to all 79 | requests: 80 | 81 | ```ts 82 | import { slowDown } from 'express-slow-down' 83 | 84 | const limiter = slowDown({ 85 | windowMs: 15 * 60 * 1000, // 15 minutes 86 | delayAfter: 5, // Allow 5 requests per 15 minutes. 87 | delayMs: (hits) => hits * 100, // Add 100 ms of delay to every request after the 5th one. 88 | 89 | /** 90 | * So: 91 | * 92 | * - requests 1-5 are not delayed. 93 | * - request 6 is delayed by 600ms 94 | * - request 7 is delayed by 700ms 95 | * - request 8 is delayed by 800ms 96 | * 97 | * and so on. After 15 minutes, the delay is reset to 0. 98 | */ 99 | }) 100 | 101 | // Apply the delay middleware to all requests. 102 | app.use(limiter) 103 | ``` 104 | 105 | To use it in a 'regular' web server (e.g. anything that uses 106 | `express.static()`), where the rate-limiter should only apply to certain 107 | requests: 108 | 109 | ```ts 110 | import { slowDown } from 'express-slow-down' 111 | 112 | const apiLimiter = slowDown({ 113 | windowMs: 15 * 60 * 1000, // 15 minutes 114 | delayAfter: 1, // Allow only one request to go at full-speed. 115 | delayMs: (hits) => hits * hits * 1000, // 2nd request has a 4 second delay, 3rd is 9 seconds, 4th is 16, etc. 116 | }) 117 | 118 | // Apply the delay middleware to API calls only. 119 | app.use('/api', apiLimiter) 120 | ``` 121 | 122 | To use a custom store: 123 | 124 | ```ts 125 | import { slowDown } from 'express-slow-down' 126 | import { MemcachedStore } from 'rate-limit-memcached' 127 | 128 | const speedLimiter = slowDown({ 129 | windowMs: 15 * 60 * 1000, // 15 minutes 130 | delayAfter: 1, // Allow only one request to go at full-speed. 131 | delayMs: (hits) => hits * hits * 1000, // Add exponential delay after 1 request. 132 | store: new MemcachedStore({ 133 | /* ... */ 134 | }), // Use the external store 135 | }) 136 | 137 | // Apply the rate limiting middleware to all requests. 138 | app.use(speedLimiter) 139 | ``` 140 | 141 | > **Note:** most stores will require additional configuration, such as custom 142 | > prefixes, when using multiple instances. The default built-in memory store is 143 | > an exception to this rule. 144 | 145 | ## Configuration 146 | 147 | ### [`windowMs`](https://express-rate-limit.mintlify.app/reference/configuration#windowms) 148 | 149 | > `number` 150 | 151 | Time frame for which requests are checked/remembered. 152 | 153 | Note that some stores have to be passed the value manually, while others infer 154 | it from the options passed to this middleware. 155 | 156 | Defaults to `60000` ms (= 1 minute). 157 | 158 | ### `delayAfter` 159 | 160 | > `number` | `function` 161 | 162 | The max number of requests allowed during `windowMs` before the middleware 163 | starts delaying responses. Can be the limit itself as a number or a (sync/async) 164 | function that accepts the Express `req` and `res` objects and then returns a 165 | number. 166 | 167 | Defaults to `1`. 168 | 169 | An example of using a function: 170 | 171 | ```ts 172 | const isPremium = async (user) => { 173 | // ... 174 | } 175 | 176 | const limiter = slowDown({ 177 | // ... 178 | delayAfter: async (req, res) => { 179 | if (await isPremium(req.user)) return 10 180 | else return 1 181 | }, 182 | }) 183 | ``` 184 | 185 | ### `delayMs` 186 | 187 | > `number | function` 188 | 189 | The delay to apply to each request once the limit is reached. Can be the delay 190 | itself (in milliseconds) as a number or a (sync/async) function that accepts a 191 | number (number of requests in the current window), the Express `req` and `res` 192 | objects and then returns a number. 193 | 194 | By default, it increases the delay by 1 second for every request over the limit: 195 | 196 | ```ts 197 | const limiter = slowDown({ 198 | // ... 199 | delayMs: (used) => (used - delayAfter) * 1000, 200 | }) 201 | ``` 202 | 203 | ### `maxDelayMs` 204 | 205 | > `number | function` 206 | 207 | The absolute maximum value for `delayMs`. After many consecutive attempts, the 208 | delay will always be this value. This option should be used especially when your 209 | application is running behind a load balancer or reverse proxy that has a 210 | request timeout. Can be the number itself (in milliseconds) or a (sync/async) 211 | function that accepts the Express `req` and `res` objects and then returns a 212 | number. 213 | 214 | Defaults to `Infinity`. 215 | 216 | For example, for the following configuration: 217 | 218 | ```ts 219 | const limiter = slowDown({ 220 | // ... 221 | delayAfter: 1, 222 | delayMs: (hits) => hits * 1000, 223 | maxDelayMs: 4000, 224 | }) 225 | ``` 226 | 227 | The first request will have no delay. The second will have a 2 second delay, the 228 | 3rd will have a 3 second delay, but the fourth, fifth, sixth, seventh and so on 229 | requests will all have a 4 second delay. 230 | 231 | ### Options from [`express-rate-limit`](https://github.com/express-rate-limit/express-rate-limit) 232 | 233 | Because 234 | [`express-rate-limit`](https://github.com/express-rate-limit/express-rate-limit) 235 | is used internally, additional options that it supports may be passed in. Some 236 | of them are listed below; see `express-rate-limit`'s 237 | [documentation](https://express-rate-limit.mintlify.app/reference/configuration) 238 | for the complete list. 239 | 240 | > **Note**: The `limit` (`max`) option is not supported (use `delayAfter` 241 | > instead), nor are `handler` or the various headers options. 242 | 243 | - [`requestPropertyName`](https://express-rate-limit.mintlify.app/reference/configuration#requestpropertyname) 244 | - [`skipFailedRequests`](https://express-rate-limit.mintlify.app/reference/configuration#skipfailedrequests) 245 | - [`skipSuccessfulRequests`](https://express-rate-limit.mintlify.app/reference/configuration#skipsuccessfulrequests) 246 | - [`keyGenerator`](https://express-rate-limit.mintlify.app/reference/configuration#keygenerator) 247 | - [`skip`](https://express-rate-limit.mintlify.app/reference/configuration#skip) 248 | - [`requestWasSuccessful`](https://express-rate-limit.mintlify.app/reference/configuration#requestwassuccessful) 249 | - [`validate`](https://express-rate-limit.mintlify.app/reference/configuration#validate) 250 | - [`store`](https://express-rate-limit.mintlify.app/reference/configuration#store) 251 | 252 | ## Request API 253 | 254 | A `req.slowDown` property is added to all requests with the `limit`, `used`, and 255 | `remaining` number of requests and, if the store provides it, a `resetTime` Date 256 | object. It also has the `delay` property, which is the amount of delay imposed 257 | on current request (milliseconds). These may be used in your application code to 258 | take additional actions or inform the user of their status. 259 | 260 | Note that `used` includes the current request, so it should always be > 0. 261 | 262 | The property name can be configured with the configuration option 263 | `requestPropertyName`. 264 | 265 | ## Issues and Contributing 266 | 267 | If you encounter a bug or want to see something added/changed, please go ahead 268 | and 269 | [open an issue](https://github.com/express-rate-limit/express-slow-down/issues/new)! 270 | If you need help with something, feel free to 271 | [start a discussion](https://github.com/express-rate-limit/express-slow-down/discussions/new)! 272 | 273 | If you wish to contribute to the library, thanks! First, please read 274 | [the contributing guide](contributing.md). Then you can pick up any issue and 275 | fix/implement it! 276 | 277 | ## License 278 | 279 | MIT © [Nathan Friedly](http://nfriedly.com/), 280 | [Vedant K](https://github.com/gamemaker1) 281 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | // /source/index.ts 2 | // Export away! 3 | 4 | // Export all the types as named exports. 5 | export * from './types.js' 6 | 7 | // Export the `slowDown` function as well. 8 | export { default, slowDown } from './slow-down.js' 9 | -------------------------------------------------------------------------------- /source/slow-down.ts: -------------------------------------------------------------------------------- 1 | // /source/express-slow-down.ts 2 | // The speed limiting middleware. 3 | 4 | import type { Request, Response, NextFunction } from 'express' 5 | import { rateLimit, type Options as RateLimitOptions } from 'express-rate-limit' 6 | import type { 7 | SlowDownRequestHandler, 8 | AugmentedRequest, 9 | SlowDownOptions, 10 | Options, 11 | } from './types' 12 | 13 | /** 14 | * Remove any options where their value is set to undefined. This avoids overwriting defaults 15 | * in the case a user passes undefined instead of simply omitting the key. 16 | * 17 | * @param passedOptions {Options} - The options to omit. 18 | * 19 | * @returns {Options} - The same options, but with all undefined fields omitted. 20 | * 21 | * @private 22 | */ 23 | const filterUndefinedOptions = ( 24 | passedOptions: Partial, 25 | ): Partial => { 26 | const filteredOptions: Partial = {} 27 | 28 | for (const k of Object.keys(passedOptions)) { 29 | const key = k as keyof Options 30 | 31 | if (passedOptions[key] !== undefined) { 32 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 33 | filteredOptions[key] = passedOptions[key] 34 | } 35 | } 36 | 37 | return filteredOptions 38 | } 39 | 40 | // Todo: consider exporting then extending express-rate-limit's ValidationError 41 | class ExpressSlowDownWarning extends Error { 42 | name: string 43 | code: string 44 | help: string 45 | constructor(code: string, message: string) { 46 | const url = `https://express-rate-limit.github.io/${code}/` 47 | 48 | super(`${message} See ${url} for more information.`) 49 | 50 | this.name = this.constructor.name 51 | this.code = code 52 | this.help = url 53 | } 54 | } 55 | 56 | /** 57 | * Create an instance of middleware that slows down responses to Express requests. 58 | * 59 | * @param passedOptions {Options} - Options to configure the speed limiter. 60 | * 61 | * @returns {SlowDownRequestHandle} - The middleware that speed-limits clients based on your configuration. 62 | * 63 | * @public 64 | */ 65 | export const slowDown = ( 66 | passedOptions: Partial = {}, 67 | ): SlowDownRequestHandler => { 68 | // Passing undefined should be equivalent to not passing an option at all, so we'll 69 | // omit all fields where their value is undefined. 70 | const notUndefinedOptions: Partial = 71 | filterUndefinedOptions(passedOptions) 72 | 73 | if ( 74 | notUndefinedOptions.headers || 75 | notUndefinedOptions.legacyHeaders || 76 | notUndefinedOptions.standardHeaders 77 | ) 78 | throw new Error( 79 | 'The headers options were removed in express-slow-down v2.0.0.', 80 | ) 81 | 82 | if ( 83 | notUndefinedOptions.max !== undefined || 84 | notUndefinedOptions.limit !== undefined 85 | ) 86 | throw new Error( 87 | 'The limit/max option is not supported by express-slow-down, please use delayAfter instead.', 88 | ) 89 | 90 | // Consolidate the validation options that have been passed by the user, and 91 | // apply them later, along with `limit: false`. 92 | const validate = 93 | typeof notUndefinedOptions.validate === 'boolean' 94 | ? { default: notUndefinedOptions.validate } 95 | : notUndefinedOptions.validate ?? { default: true } 96 | 97 | // TODO: Remove in v3. 98 | if ( 99 | typeof notUndefinedOptions.delayMs === 'number' && 100 | // Make sure the validation check is not disabled. 101 | (validate.delayMs === true || 102 | (validate.delayMs === undefined && validate.default)) 103 | ) { 104 | const message = 105 | `The behaviour of the 'delayMs' option was changed in express-slow-down v2: 106 | - For the old behavior, change the delayMs option to: 107 | 108 | delayMs: (used, req) => { 109 | const delayAfter = req.${ 110 | notUndefinedOptions.requestPropertyName ?? 'slowDown' 111 | }.limit; 112 | return (used - delayAfter) * ${notUndefinedOptions.delayMs}; 113 | }, 114 | 115 | - For the new behavior, change the delayMs option to: 116 | 117 | delayMs: () => ${notUndefinedOptions.delayMs}, 118 | 119 | Or set 'options.validate: {delayMs: false}' to disable this message.`.replace( 120 | /^(\t){3}/gm, 121 | '', 122 | ) 123 | 124 | console.warn(new ExpressSlowDownWarning('WRN_ESD_DELAYMS', message)) 125 | } 126 | 127 | // Express-rate-limit will warn about enbling or disabling unknown validations, 128 | // so delete the delayMs flag (if set) 129 | delete validate?.delayMs 130 | 131 | // See ./types.ts#Options for a detailed description of the options and their 132 | // defaults. 133 | const options: Partial & SlowDownOptions = { 134 | // The following settings are defaults that may be overridden by the user's options. 135 | delayAfter: 1, 136 | delayMs(used: number, request: AugmentedRequest, response: Response) { 137 | const delayAfter = request[options.requestPropertyName!].limit 138 | return (used - delayAfter) * 1000 139 | }, 140 | maxDelayMs: Number.POSITIVE_INFINITY, 141 | requestPropertyName: 'slowDown', 142 | 143 | // Next the user's options are pulled in, overriding defaults from above 144 | ...notUndefinedOptions, 145 | 146 | // This is a combination of the user's validate settings and our own overrides 147 | validate: { 148 | ...validate, 149 | limit: false, // We know the behavor of limit=0 changed - we depend on the new behavior! 150 | }, 151 | 152 | // These settings cannot be overriden. 153 | limit: 0, // We want the handler to run on every request. 154 | // Disable the headers, we don't want to send them. 155 | legacyHeaders: false, 156 | standardHeaders: false, 157 | // The handler contains the slow-down logic, so don't allow it to be overriden. 158 | async handler(_request: Request, response: Response, next: NextFunction) { 159 | // Get the number of requests after which we should speed-limit the client. 160 | const delayAfter = 161 | typeof options.delayAfter === 'function' 162 | ? await options.delayAfter(_request, response) 163 | : options.delayAfter 164 | 165 | // Set the limit to that value, and compute the remaining requests as well. 166 | const request = _request as AugmentedRequest 167 | const info = request[options.requestPropertyName!] 168 | info.limit = delayAfter 169 | info.remaining = Math.max(0, delayAfter - info.used) 170 | 171 | // Compute the delay, if required. 172 | let delay = 0 173 | if (info.used > delayAfter) { 174 | const unboundedDelay = 175 | typeof options.delayMs === 'function' 176 | ? await options.delayMs(info.used, request, response) 177 | : options.delayMs 178 | const maxDelayMs = 179 | typeof options.maxDelayMs === 'function' 180 | ? await options.maxDelayMs(request, response) 181 | : options.maxDelayMs 182 | 183 | // Make sure the computed delay does not exceed the max delay. 184 | delay = Math.max(0, Math.min(unboundedDelay, maxDelayMs)) 185 | } 186 | 187 | // Make sure the delay is also passed on with the request. 188 | request[options.requestPropertyName!].delay = delay 189 | 190 | // If we don't need to delay the request, send it on its way. 191 | if (delay <= 0) return next() 192 | 193 | // Otherwise, set a timer and call `next` when the timer runs out. 194 | const timerId = setTimeout(() => next(), delay) 195 | response.on('close', () => clearTimeout(timerId)) 196 | }, 197 | } 198 | 199 | // Create and return the special rate limiter. 200 | return rateLimit(options) 201 | } 202 | 203 | // Export it to the world! 204 | export default slowDown 205 | -------------------------------------------------------------------------------- /source/types.ts: -------------------------------------------------------------------------------- 1 | // /source/types.ts 2 | // All the types used by this package 3 | 4 | import type { Response } from 'express' 5 | import type { 6 | AugmentedRequest as RateLimitedRequest, 7 | RateLimitInfo, 8 | Options as RateLimitOptions, 9 | ValueDeterminingMiddleware, 10 | EnabledValidations, 11 | RateLimitRequestHandler, 12 | } from 'express-rate-limit' 13 | 14 | /** 15 | * A modified Express request handler with the rate limit and slow down methods. 16 | */ 17 | export type SlowDownRequestHandler = RateLimitRequestHandler 18 | 19 | /** 20 | * Method to generate the delay to apply to the incoming request. 21 | * 22 | * @param used {number} - The number of requests made by the client so far. 23 | * @param request {Request} - The Express request object. 24 | * @param response {Response} - The Express response object. 25 | * 26 | * @returns {number} - The delay to apply. 27 | */ 28 | export type DelayFn = ( 29 | used: number, 30 | request: AugmentedRequest, 31 | response: Response, 32 | ) => number | Promise 33 | 34 | /** 35 | * Extra validation checks provided by `express-slow-down`. 36 | */ 37 | export type ExtendedValidations = EnabledValidations & { delayMs?: boolean } 38 | 39 | /** 40 | * Options present in `express-rate-limit` that this package overrides. 41 | */ 42 | export type OverridenOptions = { 43 | /** 44 | * The header options are not supported, and using them will throw an error. 45 | */ 46 | headers?: false 47 | legacyHeaders?: false 48 | standardHeaders?: false 49 | 50 | /** 51 | * The `limit` option is set from the handler using `delayAfter`. 52 | */ 53 | limit: never 54 | max: never 55 | 56 | /** 57 | * The `handler` option is overriden by the library. 58 | */ 59 | handler: never 60 | } 61 | 62 | /** 63 | * All the `express-slow-down` specific options. 64 | */ 65 | export type SlowDownOptions = { 66 | /** 67 | * The max number of requests allowed during windowMs before the middleware 68 | * starts delaying responses. 69 | * 70 | * Can be the limit itself as a number or a (sync/async) function that accepts 71 | * the Express req and res objects and then returns a number. 72 | * 73 | * Defaults to 1. 74 | */ 75 | delayAfter: number | ValueDeterminingMiddleware 76 | 77 | /** 78 | * The delay to apply to each request once the limit is reached. 79 | * 80 | * Can be the limit itself as a number or a (sync/async) function that accepts 81 | * the Express req and res objects and then returns a number. 82 | * 83 | * By default, it increases the delay by 1 second for every request over the limit. 84 | */ 85 | delayMs: number | DelayFn 86 | 87 | /** 88 | * The absolute maximum value for delayMs. After many consecutive attempts, 89 | * the delay will always be this value. This option should be used especially 90 | * when your application is running behind a load balancer or reverse proxy 91 | * that has a request timeout. 92 | * 93 | * Defaults to infinity. 94 | */ 95 | maxDelayMs: number | ValueDeterminingMiddleware 96 | 97 | /** 98 | * Allows the developer to turn off the validation check for `delayMs` being a 99 | * function. 100 | */ 101 | validate: boolean | ExtendedValidations 102 | } 103 | 104 | /** 105 | * The configuration options for the middleware. 106 | */ 107 | export type Options = RateLimitOptions & OverridenOptions & SlowDownOptions 108 | 109 | /** 110 | * The extended request object that includes information about the client's 111 | * rate limit and delay. 112 | */ 113 | export type AugmentedRequest = RateLimitedRequest & { 114 | [key: string]: SlowDownInfo 115 | } 116 | 117 | /** 118 | * The rate limit and delay related information for each client included in the 119 | * Express request object. 120 | */ 121 | export type SlowDownInfo = RateLimitInfo & { 122 | delay: number 123 | } 124 | -------------------------------------------------------------------------------- /test/helpers/mock-stores.ts: -------------------------------------------------------------------------------- 1 | // /test/helpers/mock-stores.ts 2 | // Declares and exports legacy and modern stores to use with the middleware 3 | 4 | import type { Store, LegacyStore, IncrementCallback } from 'express-rate-limit' 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 7 | export class InvalidStore {} 8 | 9 | export class MockStore implements Store { 10 | store: { [key: string]: number } = {} 11 | incrementWasCalled = false 12 | resetKeyWasCalled = false 13 | decrementWasCalled = false 14 | 15 | async increment(key: string) { 16 | this.incrementWasCalled = true 17 | this.store[key] = (this.store[key] ?? 0) + 1 18 | 19 | return { 20 | totalHits: this.store[key], 21 | resetTime: undefined, 22 | } 23 | } 24 | 25 | decrement(key: string) { 26 | this.decrementWasCalled = true 27 | this.store[key] = (this.store[key] ?? 0) - 1 28 | } 29 | 30 | resetKey(key: string) { 31 | this.resetKeyWasCalled = true 32 | this.store[key] = 0 33 | } 34 | } 35 | 36 | export class MockLegacyStore implements LegacyStore { 37 | incrementWasCalled = false 38 | resetKeyWasCalled = false 39 | decrementWasCalled = false 40 | counter = 0 41 | 42 | incr(key: string, cb: IncrementCallback): void { 43 | this.counter++ 44 | this.incrementWasCalled = true 45 | 46 | cb(undefined, this.counter, new Date()) 47 | } 48 | 49 | decrement(key: string): void { 50 | this.counter-- 51 | this.decrementWasCalled = true 52 | } 53 | 54 | resetKey(key: string): void { 55 | this.resetKeyWasCalled = true 56 | this.counter = 0 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/helpers/requests.ts: -------------------------------------------------------------------------------- 1 | // /test/helpers/requests.ts 2 | // Exports functions that check for delay 3 | 4 | import EventEmitter from 'node:events' 5 | import type { Request, Response, NextFunction } from 'express' 6 | import { expect, jest } from '@jest/globals' 7 | 8 | /** 9 | * Converts an `EventEmitter` into an Express `req` object, at least in the 10 | * eyes of the middleware. 11 | */ 12 | const impersonateRequest = (request: any) => { 13 | request.ip = '1.2.3.4' 14 | request.app = { 15 | get: () => false, 16 | } 17 | request.headers = [] 18 | } 19 | 20 | /** 21 | * NOTE: these helpers expect timers to be mocked and setTimeout to be spied on. 22 | */ 23 | 24 | /** 25 | * Call the instance with a request and response, and make sure the request was 26 | * NOT delayed. 27 | */ 28 | export const expectNoDelay = async ( 29 | instance: any, 30 | request: any = new EventEmitter(), 31 | response: any = new EventEmitter(), 32 | ) => { 33 | const next = jest.fn() 34 | impersonateRequest(request) 35 | 36 | await instance(request as Request, response as Response, next as NextFunction) 37 | 38 | expect(setTimeout).not.toHaveBeenCalled() 39 | expect(next).toHaveBeenCalled() 40 | } 41 | 42 | /** 43 | * Call the instance with a request and response, and make sure the request was 44 | * delayed by a certain amount of time. 45 | */ 46 | export async function expectDelay( 47 | instance: any, 48 | expectedDelay: number, 49 | request: any = new EventEmitter(), 50 | response: any = new EventEmitter(), 51 | ) { 52 | const next = jest.fn() 53 | impersonateRequest(request) 54 | 55 | // Set the timeout 56 | await instance(request as Request, response as Response, next as NextFunction) 57 | expect(setTimeout).toHaveBeenCalled() 58 | expect(next).not.toHaveBeenCalled() 59 | 60 | // Wait for it... 61 | jest.advanceTimersByTime(expectedDelay - 1) 62 | expect(next).not.toHaveBeenCalled() 63 | 64 | // Now! 65 | jest.advanceTimersByTime(1) 66 | expect(next).toHaveBeenCalled() 67 | } 68 | 69 | export const expectNoDelayPromise = expectNoDelay 70 | -------------------------------------------------------------------------------- /test/helpers/server.ts: -------------------------------------------------------------------------------- 1 | // /test/helpers/server.ts 2 | // Create an Express server for testing 3 | 4 | import createApp, { 5 | type Application, 6 | type Request, 7 | type Response, 8 | type RequestHandler, 9 | } from 'express' 10 | 11 | /** 12 | * Create an Express server with the given middleware. 13 | */ 14 | export const createServer = ( 15 | middleware: RequestHandler | RequestHandler[], 16 | ): Application => { 17 | // Create an Express server, and register the middleware. 18 | const app = createApp() 19 | app.use(middleware) 20 | 21 | // Register test routes. 22 | app.all('/', (_request: Request, response: Response) => 23 | response.send('Hi there!'), 24 | ) 25 | app.get('/ip', (request: Request, response: Response) => { 26 | response.setHeader('x-your-ip', request.ip) 27 | response.sendStatus(204) 28 | }) 29 | app.all('/sleepy', middleware, (_request: Request, response: Response) => { 30 | const timerId = setTimeout(() => response.send('Hallo there!'), 100) 31 | response.on('close', () => clearTimeout(timerId)) 32 | }) 33 | app.get('/error', (_request: Request, response: Response) => 34 | response.sendStatus(400), 35 | ) 36 | app.post('/crash', (_request: Request, response: Response) => { 37 | response.emit('error', new Error('Oops!')) 38 | response.on('error', () => response.end()) 39 | }) 40 | 41 | // Return the application instance. 42 | return app 43 | } 44 | -------------------------------------------------------------------------------- /test/integration/integration-test.ts: -------------------------------------------------------------------------------- 1 | // /test/library/integration-test.ts 2 | // Tests the middleware with a real Express application. 3 | 4 | import EventEmitter from 'node:events' 5 | // eslint-disable-next-line import/no-unassigned-import 6 | import 'jest-expect-message' 7 | import { type Application } from 'express' 8 | import { agent as request } from 'supertest' 9 | import slowDown from '../../source/index.js' 10 | import { MockStore } from '../helpers/mock-stores.js' 11 | import { createServer } from '../helpers/server.js' 12 | 13 | /** 14 | * Makes the program wait for the given number of milliseconds. 15 | */ 16 | const sleep = async (milliseconds: number): Promise => 17 | // eslint-disable-next-line no-promise-executor-return 18 | new Promise((resolve) => setTimeout(resolve, milliseconds)) 19 | 20 | /** 21 | * Make a request to the default endpoint, and time it so we can check the 22 | * delay applied to the response. 23 | */ 24 | const makeTimedRequest = async (app: Application) => { 25 | const start = Date.now() 26 | await request(app) 27 | .get('/') 28 | .expect(200) 29 | .expect(/Hi there!/) 30 | return Date.now() - start 31 | } 32 | 33 | describe('integration', () => { 34 | it('should call incr on the store', async () => { 35 | const store = new MockStore() 36 | const app = createServer( 37 | slowDown({ 38 | store, 39 | validate: false, 40 | }), 41 | ) 42 | 43 | await request(app).get('/') 44 | expect(store.incrementWasCalled).toBeTruthy() 45 | }) 46 | 47 | it('should call resetKey on the store', () => { 48 | const store = new MockStore() 49 | const limiter = slowDown({ 50 | store, 51 | validate: false, 52 | }) 53 | limiter.resetKey('key') 54 | 55 | expect(store.resetKeyWasCalled).toBeTruthy() 56 | }) 57 | 58 | it('should allow the first request with minimal delay', async () => { 59 | const app = createServer(slowDown({ validate: false })) 60 | 61 | const delay = await makeTimedRequest(app) 62 | expect(delay, `First resp took too long: ${delay} ms.`).toBeLessThan(100) 63 | }) 64 | 65 | it('should apply a small delay to the second request', async () => { 66 | const app = createServer( 67 | slowDown({ 68 | delayMs: 100, 69 | validate: false, 70 | }), 71 | ) 72 | 73 | let delay = await makeTimedRequest(app) 74 | expect(delay, `First resp took too long: ${delay} ms.`).toBeLessThan(100) 75 | 76 | delay = await makeTimedRequest(app) 77 | expect( 78 | delay, 79 | `Second resp was served too quickly: ${delay} ms.`, 80 | ).toBeGreaterThanOrEqual(100) 81 | // Macos CI server is slow, and can add a 100-200ms of extra delay. 82 | expect(delay, `Second resp took too long: ${delay} ms.`).toBeLessThan(400) 83 | }) 84 | 85 | it('should apply a larger delay to the subsequent request', async () => { 86 | const app = createServer( 87 | slowDown({ 88 | delayMs: (used) => (used - 1) * 100, 89 | validate: false, 90 | }), 91 | ) 92 | 93 | await Promise.all([ 94 | request(app).get('/'), // No delay 95 | request(app).get('/'), // 100ms delay 96 | request(app).get('/'), // 200ms delay 97 | ]) 98 | const delay = await makeTimedRequest(app) 99 | 100 | // Should be about 300ms delay on 4th request - because the multiplier starts at 0 101 | // BUT, this test frequently fails with a delay in the 4-500ms range on CI. 102 | // So, loosening up the range a bit here. 103 | expect( 104 | delay >= 250 && delay <= 600, 105 | `Fourth resp was served too fast or slow: ${delay} ms.`, 106 | ).toBe(true) 107 | }) 108 | 109 | it('should apply a cap of maxDelayMs on the the delay', async () => { 110 | const app = createServer( 111 | slowDown({ 112 | delayAfter: 1, 113 | delayMs: (used) => (used - 1) * 100, 114 | maxDelayMs: 200, 115 | validate: false, 116 | }), 117 | ) 118 | 119 | await Promise.all([ 120 | request(app).get('/'), // 1st - no delay 121 | request(app).get('/'), // 2nd - 100ms delay 122 | request(app).get('/'), // 3rd - 200ms delay 123 | ]) 124 | const delay = await makeTimedRequest(app) 125 | 126 | // Should cap the delay so the 4th request delays about 200ms instead of 300ms 127 | // this one also likes to fail with too much delay on macOS in CI 128 | expect( 129 | delay, 130 | `Fourth resp was served too fast: ${delay} ms.`, 131 | ).toBeGreaterThanOrEqual(150) 132 | expect(delay, `Fourth resp was served too slow: ${delay} ms.`).toBeLessThan( 133 | 600, 134 | ) 135 | }) 136 | 137 | it('should allow delayAfter requests before delaying responses', async () => { 138 | const app = createServer( 139 | slowDown({ 140 | delayMs: 100, 141 | delayAfter: 2, 142 | validate: false, 143 | }), 144 | ) 145 | 146 | let delay = await makeTimedRequest(app) 147 | expect(delay, `First resp was served too slow: ${delay} ms.`).toBeLessThan( 148 | 50, 149 | ) 150 | 151 | delay = await makeTimedRequest(app) 152 | expect(delay, `Second resp was served too slow: ${delay} ms.`).toBeLessThan( 153 | 50, 154 | ) 155 | 156 | delay = await makeTimedRequest(app) 157 | expect( 158 | delay > 50 && delay < 150, 159 | `Third request outside of range: ${delay} ms.`, 160 | ).toBe(true) 161 | }) 162 | 163 | it('should allow delayAfter to be a function', async () => { 164 | const app = createServer( 165 | slowDown({ 166 | delayMs: 100, 167 | delayAfter: () => 2, 168 | validate: false, 169 | }), 170 | ) 171 | 172 | let delay = await makeTimedRequest(app) 173 | expect(delay, `First resp was served too slow: ${delay} ms.`).toBeLessThan( 174 | 50, 175 | ) 176 | 177 | delay = await makeTimedRequest(app) 178 | expect(delay, `Second resp was served too slow: ${delay} ms.`).toBeLessThan( 179 | 50, 180 | ) 181 | 182 | delay = await makeTimedRequest(app) 183 | expect( 184 | delay > 50 && delay < 150, 185 | `Third request outside of range: ${delay} ms.`, 186 | ).toBe(true) 187 | }) 188 | 189 | it('should (eventually) return to full speed', async () => { 190 | const app = createServer( 191 | slowDown({ 192 | delayMs: 100, 193 | delayAfter: 1, 194 | windowMs: 50, 195 | validate: false, 196 | }), 197 | ) 198 | 199 | await Promise.all([ 200 | request(app).get('/'), // 1st - no delay 201 | request(app).get('/'), // 2nd - 100ms delay 202 | request(app).get('/'), // 3rd - 200ms delay 203 | ]) 204 | 205 | await sleep(500) 206 | 207 | const delay = await makeTimedRequest(app) 208 | expect(delay, `Fourth resp was served too slow: ${delay} ms.`).toBeLessThan( 209 | 50, 210 | ) 211 | }) 212 | 213 | it('should work repeatedly (issues #2 & #3)', async () => { 214 | const app = createServer( 215 | slowDown({ 216 | delayMs: 100, 217 | delayAfter: 2, 218 | windowMs: 50, 219 | validate: false, 220 | }), 221 | ) 222 | 223 | await Promise.all([ 224 | request(app).get('/'), // 1st - no delay 225 | request(app).get('/'), // 2nd - 100ms delay 226 | request(app).get('/'), // 3rd - 200ms delay 227 | ]) 228 | await sleep(60) 229 | 230 | let delay = await makeTimedRequest(app) 231 | expect(delay, `Fourth resp was served too slow: ${delay} ms.`).toBeLessThan( 232 | 50, 233 | ) 234 | 235 | await Promise.all([ 236 | request(app).get('/'), // 1st - no delay 237 | request(app).get('/'), // 2nd - 100ms delay 238 | ]) 239 | await sleep(60) 240 | 241 | delay = await makeTimedRequest(app) 242 | expect( 243 | delay, 244 | `Eventual resp was served too slow: ${delay} ms.`, 245 | ).toBeLessThan(50) 246 | }) 247 | 248 | it('should allow individual IP to be reset', async () => { 249 | const limiter = slowDown({ 250 | delayMs: 100, 251 | delayAfter: 1, 252 | windowMs: 50, 253 | validate: false, 254 | }) 255 | const app = createServer(limiter) 256 | 257 | const response = await request(app).get('/ip').expect(204) 258 | 259 | const myIp = response.headers['x-your-ip'] 260 | if (!myIp) throw new Error('Unable to determine local IP') 261 | 262 | await request(app).get('/') // 1st - no delay 263 | await request(app).get('/') // 2nd - 100ms delay 264 | 265 | limiter.resetKey(myIp) 266 | await request(app).get('/') // 3rd - but no delay 267 | }) 268 | 269 | it('should allow custom key generators', async () => { 270 | const limiter = slowDown({ 271 | delayMs: 0, 272 | delayAfter: 2, 273 | keyGenerator: (request) => request.query.key as string, 274 | validate: false, 275 | }) 276 | const app = createServer(limiter) 277 | 278 | await request(app).get('/?key=1') // 1st - no delay 279 | await request(app).get('/?key=1') // 2nd - 100ms delay 280 | await request(app).get('/?key=2') // 1st - no delay 281 | await request(app).get('/?key=1') // 3rd - 100ms delay 282 | await request(app).get('/?key=2') // 2nd - 100ms delay 283 | await request(app).get('/?key=2') // 3rd - 100ms delay 284 | }) 285 | 286 | it('should allow custom skip function', async () => { 287 | const limiter = slowDown({ 288 | delayMs: 0, 289 | delayAfter: 2, 290 | skip: () => true, 291 | validate: false, 292 | }) 293 | const app = createServer(limiter) 294 | 295 | await request(app).get('/') 296 | await request(app).get('/') 297 | await request(app).get('/') // 3rd request would normally fail but we're skipping it 298 | }) 299 | 300 | it('should decrement hits with success response and skipSuccessfulRequests', async () => { 301 | const store = new MockStore() 302 | const app = createServer( 303 | slowDown({ 304 | skipSuccessfulRequests: true, 305 | store, 306 | validate: false, 307 | }), 308 | ) 309 | 310 | await request(app).get('/') 311 | expect( 312 | store.decrementWasCalled, 313 | '`decrement` was not called on the store', 314 | ).toBeTruthy() 315 | }) 316 | 317 | it('should decrement hits with failed response and skipFailedRequests', async () => { 318 | const store = new MockStore() 319 | const app = createServer( 320 | slowDown({ 321 | skipFailedRequests: true, 322 | store, 323 | validate: false, 324 | }), 325 | ) 326 | 327 | await request(app).get('/error').expect(400) 328 | expect( 329 | store.decrementWasCalled, 330 | '`decrement` was not called on the store', 331 | ).toBeTruthy() 332 | }) 333 | 334 | it('should decrement hits with closed response and skipFailedRequests', async () => { 335 | const store = new MockStore() 336 | 337 | const requestMock = {} 338 | const responseMock = new EventEmitter() 339 | const nextFn = () => {} 340 | const middleware = slowDown({ 341 | skipFailedRequests: true, 342 | store, 343 | validate: false, 344 | }) 345 | 346 | // eslint-disable-next-line @typescript-eslint/await-thenable 347 | await middleware(requestMock as any, responseMock as any, nextFn) 348 | responseMock.emit('close') 349 | 350 | // eslint-disable-next-line no-promise-executor-return 351 | await new Promise((resolve) => setTimeout(resolve, 200)) 352 | expect( 353 | store.decrementWasCalled, 354 | '`decrement` was not called on the store', 355 | ).toBeTruthy() 356 | }) 357 | 358 | it('should decrement hits with response emitting error and skipFailedRequests', async () => { 359 | const store = new MockStore() 360 | const app = createServer( 361 | slowDown({ 362 | skipFailedRequests: true, 363 | store, 364 | validate: false, 365 | }), 366 | ) 367 | 368 | await request(app).get('/crash') 369 | expect( 370 | store.decrementWasCalled, 371 | '`decrement` was not called on the store', 372 | ).toBeTruthy() 373 | }) 374 | 375 | it('should not decrement hits with success response and skipFailedRequests', async () => { 376 | const store = new MockStore() 377 | const app = createServer( 378 | slowDown({ 379 | skipFailedRequests: true, 380 | store, 381 | validate: false, 382 | }), 383 | ) 384 | 385 | await request(app).get('/') 386 | expect( 387 | store.decrementWasCalled, 388 | '`decrement` was called on the store', 389 | ).toBeFalsy() 390 | }) 391 | 392 | it('should not excute slow down timer in case of req closed during delay', async () => { 393 | const requestMock = {} 394 | const responseMock = new EventEmitter() 395 | const middleware = slowDown({ 396 | delayAfter: 0, 397 | delayMs: 100, 398 | windowMs: 1000, 399 | validate: false, 400 | }) 401 | const nextFn = () => { 402 | throw new Error('`setTimeout` should not excute!') 403 | } 404 | 405 | // eslint-disable-next-line @typescript-eslint/await-thenable 406 | await middleware(requestMock as any, responseMock as any, nextFn) 407 | responseMock.emit('close') 408 | 409 | // eslint-disable-next-line no-promise-executor-return 410 | await new Promise((resolve) => setTimeout(resolve, 200)) 411 | }) 412 | 413 | // TODO: it('should not excute slow down timer in case req is closed before delay begins') 414 | }) 415 | -------------------------------------------------------------------------------- /test/library/connection-test.ts: -------------------------------------------------------------------------------- 1 | // /test/library/connection-test.ts 2 | // Tests the behaviour upon abrupt connection closure 3 | 4 | import EventEmitter from 'node:events' 5 | import { jest } from '@jest/globals' 6 | import slowDown from '../../source/index.js' 7 | 8 | describe('connection', () => { 9 | beforeEach(() => { 10 | jest.useFakeTimers() 11 | jest.spyOn(global, 'setTimeout') 12 | }) 13 | afterEach(() => { 14 | jest.useRealTimers() 15 | jest.restoreAllMocks() 16 | }) 17 | 18 | it('should not excute slow down timer in case of req closed', async () => { 19 | const request = new EventEmitter() as any 20 | const res = new EventEmitter() as any 21 | 22 | // Gotta do a bunch of sillyness to convince it the request isn't finished at the start. 23 | request.socket = new EventEmitter() 24 | request.socket.readable = true 25 | request.complete = false 26 | request.readable = true 27 | res.finished = false 28 | const instance = slowDown({ 29 | skipFailedRequests: true, 30 | delayAfter: 0, 31 | delayMs: 1000, 32 | validate: false, 33 | }) 34 | const next = jest.fn() 35 | 36 | instance(request, res, next) 37 | expect(next).not.toHaveBeenCalled() 38 | 39 | // `on-finish` ignores the close event on the req/res, and only listens for 40 | // it on the socket (?) 41 | request.socket.emit('close') 42 | request.emit('close') 43 | res.emit('close') 44 | 45 | jest.advanceTimersByTime(1001) 46 | expect(next).not.toHaveBeenCalled() 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/library/delay-test.ts: -------------------------------------------------------------------------------- 1 | // /test/library/delay-test.ts 2 | // Tests the delaying mechanism 3 | 4 | import { jest } from '@jest/globals' 5 | import slowDown from '../../source/index.js' 6 | import { expectDelay, expectNoDelay } from '../helpers/requests.js' 7 | 8 | describe('slowdown', () => { 9 | beforeEach(() => { 10 | jest.useFakeTimers() 11 | jest.spyOn(global, 'setTimeout') 12 | }) 13 | afterEach(() => { 14 | jest.useRealTimers() 15 | jest.restoreAllMocks() 16 | }) 17 | 18 | it('should not delay the first request', async () => { 19 | const instance = slowDown({ 20 | validate: false, 21 | delayAfter: 1, 22 | }) 23 | 24 | await expectNoDelay(instance) 25 | }) 26 | 27 | it('should delay the first request', async () => { 28 | const instance = slowDown({ 29 | validate: false, 30 | delayAfter: 0, 31 | delayMs: 100, 32 | }) 33 | 34 | await expectDelay(instance, 100) 35 | }) 36 | 37 | it('should apply a larger delay to each subsequent request', async () => { 38 | const instance = slowDown({ 39 | validate: false, 40 | delayAfter: 0, 41 | delayMs: (used: number) => used * 100, 42 | }) 43 | 44 | await expectDelay(instance, 100) 45 | await expectDelay(instance, 200) 46 | await expectDelay(instance, 300) 47 | }) 48 | 49 | it('should apply a cap of maxDelayMs on the the delay', async () => { 50 | const instance = slowDown({ 51 | validate: false, 52 | delayAfter: 0, 53 | delayMs: (used: number) => used * 100, 54 | maxDelayMs: 250, 55 | }) 56 | 57 | await expectDelay(instance, 100) 58 | await expectDelay(instance, 200) 59 | await expectDelay(instance, 250) 60 | await expectDelay(instance, 250) 61 | await expectDelay(instance, 250) 62 | await expectDelay(instance, 250) 63 | }) 64 | 65 | it('should allow delayAfter requests before delaying', async () => { 66 | const instance = slowDown({ 67 | validate: false, 68 | delayAfter: 2, 69 | delayMs: 300, 70 | }) 71 | 72 | await expectNoDelay(instance) 73 | await expectNoDelay(instance) 74 | await expectDelay(instance, 300) 75 | }) 76 | 77 | it('should (eventually) return to full speed', async () => { 78 | const instance = slowDown({ 79 | validate: false, 80 | delayMs: 100, 81 | delayAfter: 1, 82 | windowMs: 300, 83 | }) 84 | 85 | await expectNoDelay(instance) 86 | await expectDelay(instance, 100) 87 | 88 | jest.advanceTimersByTime(200) 89 | ;(setTimeout as any).mockClear() 90 | 91 | await expectNoDelay(instance) 92 | }) 93 | 94 | it('should work repeatedly (issues #2 & #3)', async () => { 95 | const instance = slowDown({ 96 | validate: false, 97 | delayMs: 100, 98 | delayAfter: 2, 99 | windowMs: 50, 100 | }) 101 | 102 | await expectNoDelay(instance) 103 | await expectNoDelay(instance) 104 | await expectDelay(instance, 100) // Note: window is reset twice in this time 105 | ;(setTimeout as any).mockClear() 106 | 107 | await expectNoDelay(instance) 108 | await expectNoDelay(instance) 109 | await expectDelay(instance, 100) 110 | ;(setTimeout as any).mockClear() 111 | 112 | await expectNoDelay(instance) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /test/library/instance-api-test.ts: -------------------------------------------------------------------------------- 1 | // /test/library/instance-api-test.ts 2 | // Tests the instance API 3 | 4 | import { jest } from '@jest/globals' 5 | import slowDown from '../../source/index.js' 6 | import { expectDelay, expectNoDelay } from '../helpers/requests.js' 7 | 8 | describe('instance-api', () => { 9 | beforeEach(() => { 10 | jest.useFakeTimers() 11 | jest.spyOn(global, 'setTimeout') 12 | }) 13 | afterEach(() => { 14 | jest.useRealTimers() 15 | jest.restoreAllMocks() 16 | }) 17 | 18 | it('should allow individual IPs to be reset', async () => { 19 | const instance = slowDown({ 20 | validate: false, 21 | delayMs: 100, 22 | delayAfter: 1, 23 | windowMs: 1000, 24 | }) 25 | 26 | const ip = '1.2.3.4' 27 | 28 | await expectNoDelay(instance, { ip }) 29 | await expectDelay(instance, 100, { ip }) 30 | 31 | instance.resetKey(ip) 32 | ;(setTimeout as any).mockClear() 33 | await expectNoDelay(instance, { ip }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /test/library/middleware-test.ts: -------------------------------------------------------------------------------- 1 | // /test/library/middleware-test.ts 2 | // Tests the middleware by passing in various options to it 3 | 4 | import EventEmitter from 'node:events' 5 | import { jest } from '@jest/globals' 6 | import slowDown from '../../source/index.js' 7 | import { expectDelay, expectNoDelay } from '../helpers/requests.js' 8 | import { MockStore } from '../helpers/mock-stores.js' 9 | 10 | describe('middleware behaviour', () => { 11 | beforeEach(() => { 12 | jest.useFakeTimers() 13 | jest.spyOn(global, 'setTimeout') 14 | }) 15 | afterEach(() => { 16 | jest.useRealTimers() 17 | jest.restoreAllMocks() 18 | }) 19 | 20 | it('should allow delayAfter to be a function', async () => { 21 | const instance = slowDown({ 22 | validate: false, 23 | delayAfter: () => 2, 24 | delayMs: 99, 25 | }) 26 | await expectNoDelay(instance) 27 | await expectNoDelay(instance) 28 | await expectDelay(instance, 99) 29 | }) 30 | 31 | it('should allow delayMs to be a function', async () => { 32 | const instance = slowDown({ 33 | validate: false, 34 | delayAfter: 1, 35 | delayMs: () => 99, 36 | }) 37 | await expectNoDelay(instance) 38 | await expectDelay(instance, 99) 39 | }) 40 | 41 | it('should allow maxDelayMs to be a function', async () => { 42 | const instance = slowDown({ 43 | validate: false, 44 | delayAfter: 1, 45 | delayMs: (used: number) => (used - 1) * 100, 46 | maxDelayMs: () => 200, 47 | }) 48 | await expectNoDelay(instance) 49 | await expectDelay(instance, 100) 50 | await expectDelay(instance, 200) 51 | await expectDelay(instance, 200) 52 | }) 53 | 54 | it('should allow a custom key generator', async () => { 55 | const keyGenerator = jest.fn() as any 56 | const instance = slowDown({ 57 | validate: false, 58 | delayAfter: 1, 59 | keyGenerator, 60 | }) 61 | 62 | await expectNoDelay(instance) 63 | expect(keyGenerator).toHaveBeenCalled() 64 | }) 65 | 66 | it('should allow a custom skip function', async () => { 67 | const skip = jest 68 | .fn() 69 | .mockReturnValueOnce(false) 70 | .mockReturnValueOnce(true) as any 71 | const instance = slowDown({ 72 | validate: false, 73 | delayAfter: 0, 74 | delayMs: 100, 75 | skip, 76 | }) 77 | await expectDelay(instance, 100) 78 | expect(skip).toHaveBeenCalled() 79 | ;(setTimeout as any).mockClear() 80 | await expectNoDelay(instance) 81 | expect(skip).toHaveBeenCalledTimes(2) 82 | }) 83 | 84 | it('should decrement hits with success response and skipSuccessfulRequests', async () => { 85 | const request = {} 86 | const res: any = new EventEmitter() 87 | jest.spyOn(res, 'on') 88 | const store = new MockStore() 89 | const instance = slowDown({ 90 | validate: false, 91 | skipSuccessfulRequests: true, 92 | store, 93 | }) 94 | await expectNoDelay(instance, request, res) 95 | expect(store.decrementWasCalled).toBeFalsy() 96 | expect(res.on).toHaveBeenCalled() 97 | 98 | res.statusCode = 200 99 | res.emit('finish') 100 | expect(store.decrementWasCalled).toBeTruthy() 101 | }) 102 | 103 | it('should not decrement hits with error response and skipSuccessfulRequests', async () => { 104 | const request = {} 105 | const res: any = new EventEmitter() 106 | const store = new MockStore() 107 | const instance = slowDown({ 108 | validate: false, 109 | skipSuccessfulRequests: true, 110 | store, 111 | }) 112 | await expectNoDelay(instance, request, res) 113 | 114 | res.statusCode = 400 115 | res.emit('finish') 116 | expect(store.decrementWasCalled).toBeFalsy() 117 | }) 118 | 119 | it('should not decrement hits with success response and skipFailedRequests', async () => { 120 | const request = {} 121 | const res: any = new EventEmitter() 122 | jest.spyOn(res, 'on') 123 | const store = new MockStore() 124 | const instance = slowDown({ 125 | validate: false, 126 | skipFailedRequests: true, 127 | store, 128 | }) 129 | await expectNoDelay(instance, request, res) 130 | expect(res.on).toHaveBeenCalled() 131 | 132 | res.statusCode = 200 133 | res.emit('finish') 134 | expect(store.decrementWasCalled).toBeFalsy() 135 | }) 136 | 137 | it('should decrement hits with error status code and skipFailedRequests', async () => { 138 | const request = {} 139 | const res: any = new EventEmitter() 140 | const store = new MockStore() 141 | const instance = slowDown({ 142 | validate: false, 143 | skipFailedRequests: true, 144 | store, 145 | }) 146 | await expectNoDelay(instance, request, res) 147 | expect(store.decrementWasCalled).toBeFalsy() 148 | 149 | res.statusCode = 400 150 | res.emit('finish') 151 | expect(store.decrementWasCalled).toBeTruthy() 152 | }) 153 | 154 | it('should decrement hits with closed unfinished response and skipFailedRequests', async () => { 155 | const request = {} 156 | const res: any = new EventEmitter() 157 | const store = new MockStore() 158 | const instance = slowDown({ 159 | validate: false, 160 | skipFailedRequests: true, 161 | store, 162 | }) 163 | await expectNoDelay(instance, request, res) 164 | expect(store.decrementWasCalled).toBeFalsy() 165 | 166 | res.finished = false 167 | res.emit('close') 168 | expect(store.decrementWasCalled).toBeTruthy() 169 | }) 170 | 171 | it('should decrement hits with error event on response and skipFailedRequests', async () => { 172 | const request = {} 173 | const res: any = new EventEmitter() 174 | const store = new MockStore() 175 | const instance = slowDown({ 176 | validate: false, 177 | skipFailedRequests: true, 178 | store, 179 | }) 180 | await expectNoDelay(instance, request, res) 181 | expect(store.decrementWasCalled).toBeFalsy() 182 | 183 | res.emit('error') 184 | expect(store.decrementWasCalled).toBeTruthy() 185 | }) 186 | 187 | it('should augment the req object with info about the slowdown status', async () => { 188 | const request: any = {} 189 | const instance = slowDown({ 190 | validate: false, 191 | delayAfter: 2, 192 | windowMs: 1000, 193 | }) 194 | await expectNoDelay(instance, request) 195 | expect(request.slowDown).toMatchObject({ 196 | current: 1, 197 | delay: 0, 198 | limit: 2, 199 | remaining: 1, 200 | resetTime: expect.any(Date), 201 | }) 202 | }) 203 | }) 204 | -------------------------------------------------------------------------------- /test/library/options-test.ts: -------------------------------------------------------------------------------- 1 | // /test/library/options-test.ts 2 | // Tests the parsing/handling of options passed in by the user 3 | 4 | import slowDown from '../../source/index.js' 5 | import { expectNoDelay } from '../helpers/requests.js' 6 | 7 | describe('options', () => { 8 | beforeEach(() => { 9 | jest.spyOn(console, 'error').mockImplementation(() => {}) 10 | jest.spyOn(console, 'warn').mockImplementation(() => {}) 11 | }) 12 | afterEach(() => { 13 | jest.restoreAllMocks() 14 | }) 15 | 16 | it('should not modify the options object passed', () => { 17 | const options = {} 18 | slowDown(options) 19 | 20 | expect(options).toStrictEqual({}) 21 | }) 22 | 23 | it('should throw an error when header options are used', () => { 24 | // @ts-expect-error Types don't allow this, by design. 25 | expect(() => slowDown({ standardHeaders: true })).toThrow(/headers/) 26 | // @ts-expect-error Ditto. 27 | expect(() => slowDown({ legacyHeaders: true })).toThrow(/headers/) 28 | // @ts-expect-error Ditto. 29 | expect(() => slowDown({ headers: true })).toThrow(/headers/) 30 | }) 31 | 32 | it('should throw an error when max option is used', () => { 33 | // @ts-expect-error Types don't allow this, by design. 34 | expect(() => slowDown({ max: 3 })).toThrow(/delayAfter/) 35 | // @ts-expect-error Ditto. 36 | expect(() => slowDown({ limit: 3 })).toThrow(/delayAfter/) 37 | }) 38 | 39 | it('should warn about delayMs being a number', () => { 40 | slowDown({ delayMs: 100 }) 41 | expect(console.warn).toBeCalled() 42 | }) 43 | 44 | it('should not warn about delayMs being a number if validate is false', () => { 45 | slowDown({ delayMs: 100, validate: false }) 46 | expect(console.warn).not.toBeCalled() 47 | expect(console.error).not.toBeCalled() 48 | }) 49 | 50 | it('should not warn about delayMs being a number if validate.delayMs is false', () => { 51 | slowDown({ delayMs: 100, validate: { delayMs: false } }) 52 | expect(console.warn).not.toBeCalled() 53 | expect(console.error).not.toBeCalled() 54 | }) 55 | 56 | it('should not warn about max being zero when validate.delayMs is false', async () => { 57 | jest.spyOn(global, 'setTimeout') 58 | const instance = slowDown({ 59 | delayAfter: 1, 60 | delayMs: 100, 61 | validate: { delayMs: false }, 62 | }) 63 | await expectNoDelay(instance) 64 | expect(console.warn).not.toBeCalled() 65 | expect(console.error).not.toBeCalled() 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /test/library/store-test.ts: -------------------------------------------------------------------------------- 1 | // /test/library/store-test.ts 2 | // Tests the store with the middleware 3 | 4 | import { jest } from '@jest/globals' 5 | import slowDown from '../../source/index.js' 6 | import { 7 | MockStore, 8 | MockLegacyStore, 9 | InvalidStore, 10 | } from '../helpers/mock-stores.js' 11 | import { expectNoDelay, expectNoDelayPromise } from '../helpers/requests.js' 12 | 13 | describe('store', () => { 14 | beforeEach(() => { 15 | jest.useFakeTimers() 16 | jest.spyOn(global, 'setTimeout') 17 | }) 18 | afterEach(() => { 19 | jest.useRealTimers() 20 | jest.restoreAllMocks() 21 | }) 22 | 23 | describe('legacy store', () => { 24 | it('should not allow the use of a store that is not valid', async () => { 25 | expect(() => { 26 | const instance = slowDown({ 27 | validate: false, 28 | store: new InvalidStore() as any, 29 | }) 30 | 31 | console.log(instance) 32 | instance( 33 | { ip: '1' } as any, 34 | {} as any, 35 | console.log.bind(console, 'next fn') as any, 36 | ) 37 | }).toThrowError(/store/i) 38 | }) 39 | 40 | it('should call incr on the store', async () => { 41 | const store = new MockLegacyStore() 42 | expect(store.incrementWasCalled).toBeFalsy() 43 | 44 | const instance = slowDown({ 45 | validate: false, 46 | store, 47 | }) 48 | await expectNoDelay(instance) 49 | expect(store.incrementWasCalled).toBeTruthy() 50 | }) 51 | 52 | it('should call resetKey on the store', function () { 53 | const store = new MockLegacyStore() 54 | const limiter = slowDown({ 55 | validate: false, 56 | store, 57 | }) 58 | 59 | limiter.resetKey('key') 60 | expect(store.resetKeyWasCalled).toBeTruthy() 61 | }) 62 | }) 63 | 64 | describe('promise based store', () => { 65 | it('should call increment on the store', async () => { 66 | const store = new MockStore() 67 | expect(store.incrementWasCalled).toBeFalsy() 68 | 69 | const instance = slowDown({ 70 | validate: false, 71 | store, 72 | }) 73 | await expectNoDelayPromise(instance) 74 | expect(store.incrementWasCalled).toBeTruthy() 75 | }) 76 | 77 | it('should call resetKey on the store', function () { 78 | const store = new MockStore() 79 | const limiter = slowDown({ 80 | validate: false, 81 | store, 82 | }) 83 | 84 | limiter.resetKey('key') 85 | expect(store.resetKeyWasCalled).toBeTruthy() 86 | }) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@express-rate-limit/tsconfig", 3 | "include": ["source/"], 4 | "exclude": ["node_modules/"], 5 | "compilerOptions": { 6 | "target": "ES2020", 7 | "outDir": "dist" 8 | } 9 | } 10 | --------------------------------------------------------------------------------