├── .eslintignore ├── .eslintrc.yml ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ └── scorecard.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── read.js ├── types │ ├── json.js │ ├── raw.js │ ├── text.js │ └── urlencoded.js └── utils.js ├── package.json └── test ├── .eslintrc.yml ├── body-parser.js ├── json.js ├── raw.js ├── text.js ├── urlencoded.js └── utils.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: 3 | - standard 4 | - plugin:markdown/recommended 5 | plugins: 6 | - markdown 7 | overrides: 8 | - files: '**/*.md' 9 | processor: 'markdown/markdown' 10 | rules: 11 | no-param-reassign: error 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | time: "23:00" 13 | timezone: Europe/London 14 | open-pull-requests-limit: 10 15 | ignore: 16 | - dependency-name: "*" 17 | update-types: ["version-update:semver-major"] -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths-ignore: 7 | - '*.md' 8 | pull_request: 9 | paths-ignore: 10 | - '*.md' 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | lint: 16 | name: Lint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | 21 | - name: Install Node.js 22 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 23 | with: 24 | node-version: 'lts/*' 25 | 26 | - name: Install Node.js dependencies 27 | run: npm install --ignore-scripts --include=dev 28 | 29 | - name: Lint code 30 | run: npm run lint 31 | 32 | test: 33 | name: Test - Node.js ${{ matrix.node-version }} 34 | runs-on: ubuntu-latest 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | node-version: [18, 19, 20, 21, 22, 23, 24] 39 | 40 | steps: 41 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 42 | 43 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 44 | with: 45 | node-version: ${{ matrix.node-version }} 46 | check-latest: true 47 | 48 | - name: Install Node.js dependencies 49 | run: npm install 50 | 51 | - name: Run tests 52 | run: npm run test-ci 53 | 54 | - name: Upload code coverage 55 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 56 | with: 57 | name: coverage-node-${{ matrix.node-version }} 58 | path: ./coverage/lcov.info 59 | retention-days: 1 60 | 61 | coverage: 62 | needs: test 63 | runs-on: ubuntu-latest 64 | permissions: 65 | contents: read 66 | checks: write 67 | steps: 68 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 69 | 70 | - name: Install lcov 71 | shell: bash 72 | run: sudo apt-get -y install lcov 73 | 74 | - name: Collect coverage reports 75 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 76 | with: 77 | path: ./coverage 78 | pattern: coverage-node-* 79 | 80 | - name: Merge coverage reports 81 | shell: bash 82 | run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info 83 | 84 | - name: Upload coverage report 85 | uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 86 | with: 87 | file: ./lcov.info 88 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["master"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["master"] 20 | schedule: 21 | - cron: "0 0 * * 1" 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | language: [javascript, actions] 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 46 | with: 47 | languages: ${{ matrix.language }} 48 | config: | 49 | paths-ignore: 50 | - test 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | # - name: Autobuild 58 | # uses: github/codeql-action/autobuild@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 62 | 63 | # If the Autobuild fails above, remove it and uncomment the following three lines. 64 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 65 | 66 | # - run: | 67 | # echo "Run, Build Application using script" 68 | # ./location_of_script_within_repo/buildscript.sh 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '16 21 * * 1' 14 | push: 15 | branches: [ "master" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | 30 | steps: 31 | - name: "Checkout code" 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | with: 34 | persist-credentials: false 35 | 36 | - name: "Run analysis" 37 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 38 | with: 39 | results_file: results.sarif 40 | results_format: sarif 41 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 42 | # - you want to enable the Branch-Protection check on a *public* repository, or 43 | # - you are installing Scorecard on a *private* repository 44 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 45 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 46 | 47 | # Public repositories: 48 | # - Publish results to OpenSSF REST API for easy access by consumers 49 | # - Allows the repository to include the Scorecard badge. 50 | # - See https://github.com/ossf/scorecard-action#publishing-results. 51 | # For private repositories: 52 | # - `publish_results` will always be set to `false`, regardless 53 | # of the value entered here. 54 | publish_results: true 55 | 56 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 57 | # format to the repository Actions tab. 58 | - name: "Upload artifact" 59 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 60 | with: 61 | name: SARIF file 62 | path: results.sarif 63 | retention-days: 5 64 | 65 | # Upload the results to GitHub's code scanning dashboard. 66 | - name: "Upload to code-scanning" 67 | uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 68 | with: 69 | sarif_file: results.sarif 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | unreleased 2 | ========================= 3 | 4 | * deps: 5 | * type-is@^2.0.1 6 | 7 | 2.2.0 / 2025-03-27 8 | ========================= 9 | 10 | * refactor: normalize common options for all parsers 11 | * deps: 12 | * iconv-lite@^0.6.3 13 | 14 | 2.1.0 / 2025-02-10 15 | ========================= 16 | 17 | * deps: 18 | * type-is@^2.0.0 19 | * debug@^4.4.0 20 | * Removed destroy 21 | * refactor: prefix built-in node module imports 22 | * use the node require cache instead of custom caching 23 | 24 | 2.0.2 / 2024-10-31 25 | ========================= 26 | 27 | * remove `unpipe` package and use native `unpipe()` method 28 | 29 | 2.0.1 / 2024-09-10 30 | ========================= 31 | 32 | * Restore expected behavior `extended` to `false` 33 | 34 | 2.0.0 / 2024-09-10 35 | ========================= 36 | * Propagate changes from 1.20.3 37 | * add brotli support #406 38 | * Breaking Change: Node.js 18 is the minimum supported version 39 | 40 | 2.0.0-beta.2 / 2023-02-23 41 | ========================= 42 | 43 | This incorporates all changes after 1.19.1 up to 1.20.2. 44 | 45 | * Remove deprecated `bodyParser()` combination middleware 46 | * deps: debug@3.1.0 47 | - Add `DEBUG_HIDE_DATE` environment variable 48 | - Change timer to per-namespace instead of global 49 | - Change non-TTY date format 50 | - Remove `DEBUG_FD` environment variable support 51 | - Support 256 namespace colors 52 | * deps: iconv-lite@0.5.2 53 | - Add encoding cp720 54 | - Add encoding UTF-32 55 | * deps: raw-body@3.0.0-beta.1 56 | 57 | 2.0.0-beta.1 / 2021-12-17 58 | ========================= 59 | 60 | * Drop support for Node.js 0.8 61 | * `req.body` is no longer always initialized to `{}` 62 | - it is left `undefined` unless a body is parsed 63 | * `urlencoded` parser now defaults `extended` to `false` 64 | * Use `on-finished` to determine when body read 65 | 66 | 1.20.3 / 2024-09-10 67 | =================== 68 | 69 | * deps: qs@6.13.0 70 | * add `depth` option to customize the depth level in the parser 71 | * IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`) 72 | 73 | 1.20.2 / 2023-02-21 74 | =================== 75 | 76 | * Fix strict json error message on Node.js 19+ 77 | * deps: content-type@~1.0.5 78 | - perf: skip value escaping when unnecessary 79 | * deps: raw-body@2.5.2 80 | 81 | 1.20.1 / 2022-10-06 82 | =================== 83 | 84 | * deps: qs@6.11.0 85 | * perf: remove unnecessary object clone 86 | 87 | 1.20.0 / 2022-04-02 88 | =================== 89 | 90 | * Fix error message for json parse whitespace in `strict` 91 | * Fix internal error when inflated body exceeds limit 92 | * Prevent loss of async hooks context 93 | * Prevent hanging when request already read 94 | * deps: depd@2.0.0 95 | - Replace internal `eval` usage with `Function` constructor 96 | - Use instance methods on `process` to check for listeners 97 | * deps: http-errors@2.0.0 98 | - deps: depd@2.0.0 99 | - deps: statuses@2.0.1 100 | * deps: on-finished@2.4.1 101 | * deps: qs@6.10.3 102 | * deps: raw-body@2.5.1 103 | - deps: http-errors@2.0.0 104 | 105 | 1.19.2 / 2022-02-15 106 | =================== 107 | 108 | * deps: bytes@3.1.2 109 | * deps: qs@6.9.7 110 | * Fix handling of `__proto__` keys 111 | * deps: raw-body@2.4.3 112 | - deps: bytes@3.1.2 113 | 114 | 1.19.1 / 2021-12-10 115 | =================== 116 | 117 | * deps: bytes@3.1.1 118 | * deps: http-errors@1.8.1 119 | - deps: inherits@2.0.4 120 | - deps: toidentifier@1.0.1 121 | - deps: setprototypeof@1.2.0 122 | * deps: qs@6.9.6 123 | * deps: raw-body@2.4.2 124 | - deps: bytes@3.1.1 125 | - deps: http-errors@1.8.1 126 | * deps: safe-buffer@5.2.1 127 | * deps: type-is@~1.6.18 128 | 129 | 1.19.0 / 2019-04-25 130 | =================== 131 | 132 | * deps: bytes@3.1.0 133 | - Add petabyte (`pb`) support 134 | * deps: http-errors@1.7.2 135 | - Set constructor name when possible 136 | - deps: setprototypeof@1.1.1 137 | - deps: statuses@'>= 1.5.0 < 2' 138 | * deps: iconv-lite@0.4.24 139 | - Added encoding MIK 140 | * deps: qs@6.7.0 141 | - Fix parsing array brackets after index 142 | * deps: raw-body@2.4.0 143 | - deps: bytes@3.1.0 144 | - deps: http-errors@1.7.2 145 | - deps: iconv-lite@0.4.24 146 | * deps: type-is@~1.6.17 147 | - deps: mime-types@~2.1.24 148 | - perf: prevent internal `throw` on invalid type 149 | 150 | 1.18.3 / 2018-05-14 151 | =================== 152 | 153 | * Fix stack trace for strict json parse error 154 | * deps: depd@~1.1.2 155 | - perf: remove argument reassignment 156 | * deps: http-errors@~1.6.3 157 | - deps: depd@~1.1.2 158 | - deps: setprototypeof@1.1.0 159 | - deps: statuses@'>= 1.3.1 < 2' 160 | * deps: iconv-lite@0.4.23 161 | - Fix loading encoding with year appended 162 | - Fix deprecation warnings on Node.js 10+ 163 | * deps: qs@6.5.2 164 | * deps: raw-body@2.3.3 165 | - deps: http-errors@1.6.3 166 | - deps: iconv-lite@0.4.23 167 | * deps: type-is@~1.6.16 168 | - deps: mime-types@~2.1.18 169 | 170 | 1.18.2 / 2017-09-22 171 | =================== 172 | 173 | * deps: debug@2.6.9 174 | * perf: remove argument reassignment 175 | 176 | 1.18.1 / 2017-09-12 177 | =================== 178 | 179 | * deps: content-type@~1.0.4 180 | - perf: remove argument reassignment 181 | - perf: skip parameter parsing when no parameters 182 | * deps: iconv-lite@0.4.19 183 | - Fix ISO-8859-1 regression 184 | - Update Windows-1255 185 | * deps: qs@6.5.1 186 | - Fix parsing & compacting very deep objects 187 | * deps: raw-body@2.3.2 188 | - deps: iconv-lite@0.4.19 189 | 190 | 1.18.0 / 2017-09-08 191 | =================== 192 | 193 | * Fix JSON strict violation error to match native parse error 194 | * Include the `body` property on verify errors 195 | * Include the `type` property on all generated errors 196 | * Use `http-errors` to set status code on errors 197 | * deps: bytes@3.0.0 198 | * deps: debug@2.6.8 199 | * deps: depd@~1.1.1 200 | - Remove unnecessary `Buffer` loading 201 | * deps: http-errors@~1.6.2 202 | - deps: depd@1.1.1 203 | * deps: iconv-lite@0.4.18 204 | - Add support for React Native 205 | - Add a warning if not loaded as utf-8 206 | - Fix CESU-8 decoding in Node.js 8 207 | - Improve speed of ISO-8859-1 encoding 208 | * deps: qs@6.5.0 209 | * deps: raw-body@2.3.1 210 | - Use `http-errors` for standard emitted errors 211 | - deps: bytes@3.0.0 212 | - deps: iconv-lite@0.4.18 213 | - perf: skip buffer decoding on overage chunk 214 | * perf: prevent internal `throw` when missing charset 215 | 216 | 1.17.2 / 2017-05-17 217 | =================== 218 | 219 | * deps: debug@2.6.7 220 | - Fix `DEBUG_MAX_ARRAY_LENGTH` 221 | - deps: ms@2.0.0 222 | * deps: type-is@~1.6.15 223 | - deps: mime-types@~2.1.15 224 | 225 | 1.17.1 / 2017-03-06 226 | =================== 227 | 228 | * deps: qs@6.4.0 229 | - Fix regression parsing keys starting with `[` 230 | 231 | 1.17.0 / 2017-03-01 232 | =================== 233 | 234 | * deps: http-errors@~1.6.1 235 | - Make `message` property enumerable for `HttpError`s 236 | - deps: setprototypeof@1.0.3 237 | * deps: qs@6.3.1 238 | - Fix compacting nested arrays 239 | 240 | 1.16.1 / 2017-02-10 241 | =================== 242 | 243 | * deps: debug@2.6.1 244 | - Fix deprecation messages in WebStorm and other editors 245 | - Undeprecate `DEBUG_FD` set to `1` or `2` 246 | 247 | 1.16.0 / 2017-01-17 248 | =================== 249 | 250 | * deps: debug@2.6.0 251 | - Allow colors in workers 252 | - Deprecated `DEBUG_FD` environment variable 253 | - Fix error when running under React Native 254 | - Use same color for same namespace 255 | - deps: ms@0.7.2 256 | * deps: http-errors@~1.5.1 257 | - deps: inherits@2.0.3 258 | - deps: setprototypeof@1.0.2 259 | - deps: statuses@'>= 1.3.1 < 2' 260 | * deps: iconv-lite@0.4.15 261 | - Added encoding MS-31J 262 | - Added encoding MS-932 263 | - Added encoding MS-936 264 | - Added encoding MS-949 265 | - Added encoding MS-950 266 | - Fix GBK/GB18030 handling of Euro character 267 | * deps: qs@6.2.1 268 | - Fix array parsing from skipping empty values 269 | * deps: raw-body@~2.2.0 270 | - deps: iconv-lite@0.4.15 271 | * deps: type-is@~1.6.14 272 | - deps: mime-types@~2.1.13 273 | 274 | 1.15.2 / 2016-06-19 275 | =================== 276 | 277 | * deps: bytes@2.4.0 278 | * deps: content-type@~1.0.2 279 | - perf: enable strict mode 280 | * deps: http-errors@~1.5.0 281 | - Use `setprototypeof` module to replace `__proto__` setting 282 | - deps: statuses@'>= 1.3.0 < 2' 283 | - perf: enable strict mode 284 | * deps: qs@6.2.0 285 | * deps: raw-body@~2.1.7 286 | - deps: bytes@2.4.0 287 | - perf: remove double-cleanup on happy path 288 | * deps: type-is@~1.6.13 289 | - deps: mime-types@~2.1.11 290 | 291 | 1.15.1 / 2016-05-05 292 | =================== 293 | 294 | * deps: bytes@2.3.0 295 | - Drop partial bytes on all parsed units 296 | - Fix parsing byte string that looks like hex 297 | * deps: raw-body@~2.1.6 298 | - deps: bytes@2.3.0 299 | * deps: type-is@~1.6.12 300 | - deps: mime-types@~2.1.10 301 | 302 | 1.15.0 / 2016-02-10 303 | =================== 304 | 305 | * deps: http-errors@~1.4.0 306 | - Add `HttpError` export, for `err instanceof createError.HttpError` 307 | - deps: inherits@2.0.1 308 | - deps: statuses@'>= 1.2.1 < 2' 309 | * deps: qs@6.1.0 310 | * deps: type-is@~1.6.11 311 | - deps: mime-types@~2.1.9 312 | 313 | 1.14.2 / 2015-12-16 314 | =================== 315 | 316 | * deps: bytes@2.2.0 317 | * deps: iconv-lite@0.4.13 318 | * deps: qs@5.2.0 319 | * deps: raw-body@~2.1.5 320 | - deps: bytes@2.2.0 321 | - deps: iconv-lite@0.4.13 322 | * deps: type-is@~1.6.10 323 | - deps: mime-types@~2.1.8 324 | 325 | 1.14.1 / 2015-09-27 326 | =================== 327 | 328 | * Fix issue where invalid charset results in 400 when `verify` used 329 | * deps: iconv-lite@0.4.12 330 | - Fix CESU-8 decoding in Node.js 4.x 331 | * deps: raw-body@~2.1.4 332 | - Fix masking critical errors from `iconv-lite` 333 | - deps: iconv-lite@0.4.12 334 | * deps: type-is@~1.6.9 335 | - deps: mime-types@~2.1.7 336 | 337 | 1.14.0 / 2015-09-16 338 | =================== 339 | 340 | * Fix JSON strict parse error to match syntax errors 341 | * Provide static `require` analysis in `urlencoded` parser 342 | * deps: depd@~1.1.0 343 | - Support web browser loading 344 | * deps: qs@5.1.0 345 | * deps: raw-body@~2.1.3 346 | - Fix sync callback when attaching data listener causes sync read 347 | * deps: type-is@~1.6.8 348 | - Fix type error when given invalid type to match against 349 | - deps: mime-types@~2.1.6 350 | 351 | 1.13.3 / 2015-07-31 352 | =================== 353 | 354 | * deps: type-is@~1.6.6 355 | - deps: mime-types@~2.1.4 356 | 357 | 1.13.2 / 2015-07-05 358 | =================== 359 | 360 | * deps: iconv-lite@0.4.11 361 | * deps: qs@4.0.0 362 | - Fix dropping parameters like `hasOwnProperty` 363 | - Fix user-visible incompatibilities from 3.1.0 364 | - Fix various parsing edge cases 365 | * deps: raw-body@~2.1.2 366 | - Fix error stack traces to skip `makeError` 367 | - deps: iconv-lite@0.4.11 368 | * deps: type-is@~1.6.4 369 | - deps: mime-types@~2.1.2 370 | - perf: enable strict mode 371 | - perf: remove argument reassignment 372 | 373 | 1.13.1 / 2015-06-16 374 | =================== 375 | 376 | * deps: qs@2.4.2 377 | - Downgraded from 3.1.0 because of user-visible incompatibilities 378 | 379 | 1.13.0 / 2015-06-14 380 | =================== 381 | 382 | * Add `statusCode` property on `Error`s, in addition to `status` 383 | * Change `type` default to `application/json` for JSON parser 384 | * Change `type` default to `application/x-www-form-urlencoded` for urlencoded parser 385 | * Provide static `require` analysis 386 | * Use the `http-errors` module to generate errors 387 | * deps: bytes@2.1.0 388 | - Slight optimizations 389 | * deps: iconv-lite@0.4.10 390 | - The encoding UTF-16 without BOM now defaults to UTF-16LE when detection fails 391 | - Leading BOM is now removed when decoding 392 | * deps: on-finished@~2.3.0 393 | - Add defined behavior for HTTP `CONNECT` requests 394 | - Add defined behavior for HTTP `Upgrade` requests 395 | - deps: ee-first@1.1.1 396 | * deps: qs@3.1.0 397 | - Fix dropping parameters like `hasOwnProperty` 398 | - Fix various parsing edge cases 399 | - Parsed object now has `null` prototype 400 | * deps: raw-body@~2.1.1 401 | - Use `unpipe` module for unpiping requests 402 | - deps: iconv-lite@0.4.10 403 | * deps: type-is@~1.6.3 404 | - deps: mime-types@~2.1.1 405 | - perf: reduce try block size 406 | - perf: remove bitwise operations 407 | * perf: enable strict mode 408 | * perf: remove argument reassignment 409 | * perf: remove delete call 410 | 411 | 1.12.4 / 2015-05-10 412 | =================== 413 | 414 | * deps: debug@~2.2.0 415 | * deps: qs@2.4.2 416 | - Fix allowing parameters like `constructor` 417 | * deps: on-finished@~2.2.1 418 | * deps: raw-body@~2.0.1 419 | - Fix a false-positive when unpiping in Node.js 0.8 420 | - deps: bytes@2.0.1 421 | * deps: type-is@~1.6.2 422 | - deps: mime-types@~2.0.11 423 | 424 | 1.12.3 / 2015-04-15 425 | =================== 426 | 427 | * Slight efficiency improvement when not debugging 428 | * deps: depd@~1.0.1 429 | * deps: iconv-lite@0.4.8 430 | - Add encoding alias UNICODE-1-1-UTF-7 431 | * deps: raw-body@1.3.4 432 | - Fix hanging callback if request aborts during read 433 | - deps: iconv-lite@0.4.8 434 | 435 | 1.12.2 / 2015-03-16 436 | =================== 437 | 438 | * deps: qs@2.4.1 439 | - Fix error when parameter `hasOwnProperty` is present 440 | 441 | 1.12.1 / 2015-03-15 442 | =================== 443 | 444 | * deps: debug@~2.1.3 445 | - Fix high intensity foreground color for bold 446 | - deps: ms@0.7.0 447 | * deps: type-is@~1.6.1 448 | - deps: mime-types@~2.0.10 449 | 450 | 1.12.0 / 2015-02-13 451 | =================== 452 | 453 | * add `debug` messages 454 | * accept a function for the `type` option 455 | * use `content-type` to parse `Content-Type` headers 456 | * deps: iconv-lite@0.4.7 457 | - Gracefully support enumerables on `Object.prototype` 458 | * deps: raw-body@1.3.3 459 | - deps: iconv-lite@0.4.7 460 | * deps: type-is@~1.6.0 461 | - fix argument reassignment 462 | - fix false-positives in `hasBody` `Transfer-Encoding` check 463 | - support wildcard for both type and subtype (`*/*`) 464 | - deps: mime-types@~2.0.9 465 | 466 | 1.11.0 / 2015-01-30 467 | =================== 468 | 469 | * make internal `extended: true` depth limit infinity 470 | * deps: type-is@~1.5.6 471 | - deps: mime-types@~2.0.8 472 | 473 | 1.10.2 / 2015-01-20 474 | =================== 475 | 476 | * deps: iconv-lite@0.4.6 477 | - Fix rare aliases of single-byte encodings 478 | * deps: raw-body@1.3.2 479 | - deps: iconv-lite@0.4.6 480 | 481 | 1.10.1 / 2015-01-01 482 | =================== 483 | 484 | * deps: on-finished@~2.2.0 485 | * deps: type-is@~1.5.5 486 | - deps: mime-types@~2.0.7 487 | 488 | 1.10.0 / 2014-12-02 489 | =================== 490 | 491 | * make internal `extended: true` array limit dynamic 492 | 493 | 1.9.3 / 2014-11-21 494 | ================== 495 | 496 | * deps: iconv-lite@0.4.5 497 | - Fix Windows-31J and X-SJIS encoding support 498 | * deps: qs@2.3.3 499 | - Fix `arrayLimit` behavior 500 | * deps: raw-body@1.3.1 501 | - deps: iconv-lite@0.4.5 502 | * deps: type-is@~1.5.3 503 | - deps: mime-types@~2.0.3 504 | 505 | 1.9.2 / 2014-10-27 506 | ================== 507 | 508 | * deps: qs@2.3.2 509 | - Fix parsing of mixed objects and values 510 | 511 | 1.9.1 / 2014-10-22 512 | ================== 513 | 514 | * deps: on-finished@~2.1.1 515 | - Fix handling of pipelined requests 516 | * deps: qs@2.3.0 517 | - Fix parsing of mixed implicit and explicit arrays 518 | * deps: type-is@~1.5.2 519 | - deps: mime-types@~2.0.2 520 | 521 | 1.9.0 / 2014-09-24 522 | ================== 523 | 524 | * include the charset in "unsupported charset" error message 525 | * include the encoding in "unsupported content encoding" error message 526 | * deps: depd@~1.0.0 527 | 528 | 1.8.4 / 2014-09-23 529 | ================== 530 | 531 | * fix content encoding to be case-insensitive 532 | 533 | 1.8.3 / 2014-09-19 534 | ================== 535 | 536 | * deps: qs@2.2.4 537 | - Fix issue with object keys starting with numbers truncated 538 | 539 | 1.8.2 / 2014-09-15 540 | ================== 541 | 542 | * deps: depd@0.4.5 543 | 544 | 1.8.1 / 2014-09-07 545 | ================== 546 | 547 | * deps: media-typer@0.3.0 548 | * deps: type-is@~1.5.1 549 | 550 | 1.8.0 / 2014-09-05 551 | ================== 552 | 553 | * make empty-body-handling consistent between chunked requests 554 | - empty `json` produces `{}` 555 | - empty `raw` produces `new Buffer(0)` 556 | - empty `text` produces `''` 557 | - empty `urlencoded` produces `{}` 558 | * deps: qs@2.2.3 559 | - Fix issue where first empty value in array is discarded 560 | * deps: type-is@~1.5.0 561 | - fix `hasbody` to be true for `content-length: 0` 562 | 563 | 1.7.0 / 2014-09-01 564 | ================== 565 | 566 | * add `parameterLimit` option to `urlencoded` parser 567 | * change `urlencoded` extended array limit to 100 568 | * respond with 413 when over `parameterLimit` in `urlencoded` 569 | 570 | 1.6.7 / 2014-08-29 571 | ================== 572 | 573 | * deps: qs@2.2.2 574 | - Remove unnecessary cloning 575 | 576 | 1.6.6 / 2014-08-27 577 | ================== 578 | 579 | * deps: qs@2.2.0 580 | - Array parsing fix 581 | - Performance improvements 582 | 583 | 1.6.5 / 2014-08-16 584 | ================== 585 | 586 | * deps: on-finished@2.1.0 587 | 588 | 1.6.4 / 2014-08-14 589 | ================== 590 | 591 | * deps: qs@1.2.2 592 | 593 | 1.6.3 / 2014-08-10 594 | ================== 595 | 596 | * deps: qs@1.2.1 597 | 598 | 1.6.2 / 2014-08-07 599 | ================== 600 | 601 | * deps: qs@1.2.0 602 | - Fix parsing array of objects 603 | 604 | 1.6.1 / 2014-08-06 605 | ================== 606 | 607 | * deps: qs@1.1.0 608 | - Accept urlencoded square brackets 609 | - Accept empty values in implicit array notation 610 | 611 | 1.6.0 / 2014-08-05 612 | ================== 613 | 614 | * deps: qs@1.0.2 615 | - Complete rewrite 616 | - Limits array length to 20 617 | - Limits object depth to 5 618 | - Limits parameters to 1,000 619 | 620 | 1.5.2 / 2014-07-27 621 | ================== 622 | 623 | * deps: depd@0.4.4 624 | - Work-around v8 generating empty stack traces 625 | 626 | 1.5.1 / 2014-07-26 627 | ================== 628 | 629 | * deps: depd@0.4.3 630 | - Fix exception when global `Error.stackTraceLimit` is too low 631 | 632 | 1.5.0 / 2014-07-20 633 | ================== 634 | 635 | * deps: depd@0.4.2 636 | - Add `TRACE_DEPRECATION` environment variable 637 | - Remove non-standard grey color from color output 638 | - Support `--no-deprecation` argument 639 | - Support `--trace-deprecation` argument 640 | * deps: iconv-lite@0.4.4 641 | - Added encoding UTF-7 642 | * deps: raw-body@1.3.0 643 | - deps: iconv-lite@0.4.4 644 | - Added encoding UTF-7 645 | - Fix `Cannot switch to old mode now` error on Node.js 0.10+ 646 | * deps: type-is@~1.3.2 647 | 648 | 1.4.3 / 2014-06-19 649 | ================== 650 | 651 | * deps: type-is@1.3.1 652 | - fix global variable leak 653 | 654 | 1.4.2 / 2014-06-19 655 | ================== 656 | 657 | * deps: type-is@1.3.0 658 | - improve type parsing 659 | 660 | 1.4.1 / 2014-06-19 661 | ================== 662 | 663 | * fix urlencoded extended deprecation message 664 | 665 | 1.4.0 / 2014-06-19 666 | ================== 667 | 668 | * add `text` parser 669 | * add `raw` parser 670 | * check accepted charset in content-type (accepts utf-8) 671 | * check accepted encoding in content-encoding (accepts identity) 672 | * deprecate `bodyParser()` middleware; use `.json()` and `.urlencoded()` as needed 673 | * deprecate `urlencoded()` without provided `extended` option 674 | * lazy-load urlencoded parsers 675 | * parsers split into files for reduced mem usage 676 | * support gzip and deflate bodies 677 | - set `inflate: false` to turn off 678 | * deps: raw-body@1.2.2 679 | - Support all encodings from `iconv-lite` 680 | 681 | 1.3.1 / 2014-06-11 682 | ================== 683 | 684 | * deps: type-is@1.2.1 685 | - Switch dependency from mime to mime-types@1.0.0 686 | 687 | 1.3.0 / 2014-05-31 688 | ================== 689 | 690 | * add `extended` option to urlencoded parser 691 | 692 | 1.2.2 / 2014-05-27 693 | ================== 694 | 695 | * deps: raw-body@1.1.6 696 | - assert stream encoding on node.js 0.8 697 | - assert stream encoding on node.js < 0.10.6 698 | - deps: bytes@1 699 | 700 | 1.2.1 / 2014-05-26 701 | ================== 702 | 703 | * invoke `next(err)` after request fully read 704 | - prevents hung responses and socket hang ups 705 | 706 | 1.2.0 / 2014-05-11 707 | ================== 708 | 709 | * add `verify` option 710 | * deps: type-is@1.2.0 711 | - support suffix matching 712 | 713 | 1.1.2 / 2014-05-11 714 | ================== 715 | 716 | * improve json parser speed 717 | 718 | 1.1.1 / 2014-05-11 719 | ================== 720 | 721 | * fix repeated limit parsing with every request 722 | 723 | 1.1.0 / 2014-05-10 724 | ================== 725 | 726 | * add `type` option 727 | * deps: pin for safety and consistency 728 | 729 | 1.0.2 / 2014-04-14 730 | ================== 731 | 732 | * use `type-is` module 733 | 734 | 1.0.1 / 2014-03-20 735 | ================== 736 | 737 | * lower default limits to 100kb 738 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Jonathan Ong 4 | Copyright (c) 2014-2015 Douglas Christopher Wilson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # body-parser 2 | 3 | [![NPM Version][npm-version-image]][npm-url] 4 | [![NPM Downloads][npm-downloads-image]][npm-url] 5 | [![Build Status][ci-image]][ci-url] 6 | [![Test Coverage][coveralls-image]][coveralls-url] 7 | [![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] 8 | 9 | Node.js body parsing middleware. 10 | 11 | Parse incoming request bodies in a middleware before your handlers, available 12 | under the `req.body` property. 13 | 14 | **Note** As `req.body`'s shape is based on user-controlled input, all 15 | properties and values in this object are untrusted and should be validated 16 | before trusting. For example, `req.body.foo.toString()` may fail in multiple 17 | ways, for example the `foo` property may not be there or may not be a string, 18 | and `toString` may not be a function and instead a string or other user input. 19 | 20 | [Learn about the anatomy of an HTTP transaction in Node.js](https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/). 21 | 22 | _This does not handle multipart bodies_, due to their complex and typically 23 | large nature. For multipart bodies, you may be interested in the following 24 | modules: 25 | 26 | * [busboy](https://www.npmjs.org/package/busboy#readme) and 27 | [connect-busboy](https://www.npmjs.org/package/connect-busboy#readme) 28 | * [multiparty](https://www.npmjs.org/package/multiparty#readme) and 29 | [connect-multiparty](https://www.npmjs.org/package/connect-multiparty#readme) 30 | * [formidable](https://www.npmjs.org/package/formidable#readme) 31 | * [multer](https://www.npmjs.org/package/multer#readme) 32 | 33 | This module provides the following parsers: 34 | 35 | * [JSON body parser](#bodyparserjsonoptions) 36 | * [Raw body parser](#bodyparserrawoptions) 37 | * [Text body parser](#bodyparsertextoptions) 38 | * [URL-encoded form body parser](#bodyparserurlencodedoptions) 39 | 40 | Other body parsers you might be interested in: 41 | 42 | - [body](https://www.npmjs.org/package/body#readme) 43 | - [co-body](https://www.npmjs.org/package/co-body#readme) 44 | 45 | ## Installation 46 | 47 | ```sh 48 | $ npm install body-parser 49 | ``` 50 | 51 | ## API 52 | 53 | ```js 54 | const bodyParser = require('body-parser') 55 | ``` 56 | 57 | The `bodyParser` object exposes various factories to create middlewares. All 58 | middlewares will populate the `req.body` property with the parsed body when 59 | the `Content-Type` request header matches the `type` option. 60 | 61 | The various errors returned by this module are described in the 62 | [errors section](#errors). 63 | 64 | ### bodyParser.json([options]) 65 | 66 | Returns middleware that only parses `json` and only looks at requests where 67 | the `Content-Type` header matches the `type` option. This parser accepts any 68 | Unicode encoding of the body and supports automatic inflation of `gzip`, 69 | `br` (brotli) and `deflate` encodings. 70 | 71 | A new `body` object containing the parsed data is populated on the `request` 72 | object after the middleware (i.e. `req.body`). 73 | 74 | #### Options 75 | 76 | The `json` function takes an optional `options` object that may contain any of 77 | the following keys: 78 | 79 | ##### inflate 80 | 81 | When set to `true`, then deflated (compressed) bodies will be inflated; when 82 | `false`, deflated bodies are rejected. Defaults to `true`. 83 | 84 | ##### limit 85 | 86 | Controls the maximum request body size. If this is a number, then the value 87 | specifies the number of bytes; if it is a string, the value is passed to the 88 | [bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults 89 | to `'100kb'`. 90 | 91 | ##### reviver 92 | 93 | The `reviver` option is passed directly to `JSON.parse` as the second 94 | argument. You can find more information on this argument 95 | [in the MDN documentation about JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Example.3A_Using_the_reviver_parameter). 96 | 97 | ##### strict 98 | 99 | When set to `true`, will only accept arrays and objects; when `false` will 100 | accept anything `JSON.parse` accepts. Defaults to `true`. 101 | 102 | ##### type 103 | 104 | The `type` option is used to determine what media type the middleware will 105 | parse. This option can be a string, array of strings, or a function. If not a 106 | function, `type` option is passed directly to the 107 | [type-is](https://www.npmjs.org/package/type-is#readme) library and this can 108 | be an extension name (like `json`), a mime type (like `application/json`), or 109 | a mime type with a wildcard (like `*/*` or `*/json`). If a function, the `type` 110 | option is called as `fn(req)` and the request is parsed if it returns a truthy 111 | value. Defaults to `application/json`. 112 | 113 | ##### verify 114 | 115 | The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, 116 | where `buf` is a `Buffer` of the raw request body and `encoding` is the 117 | encoding of the request. The parsing can be aborted by throwing an error. 118 | 119 | ### bodyParser.raw([options]) 120 | 121 | Returns middleware that parses all bodies as a `Buffer` and only looks at 122 | requests where the `Content-Type` header matches the `type` option. This 123 | parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate` 124 | encodings. 125 | 126 | A new `body` object containing the parsed data is populated on the `request` 127 | object after the middleware (i.e. `req.body`). This will be a `Buffer` object 128 | of the body. 129 | 130 | #### Options 131 | 132 | The `raw` function takes an optional `options` object that may contain any of 133 | the following keys: 134 | 135 | ##### inflate 136 | 137 | When set to `true`, then deflated (compressed) bodies will be inflated; when 138 | `false`, deflated bodies are rejected. Defaults to `true`. 139 | 140 | ##### limit 141 | 142 | Controls the maximum request body size. If this is a number, then the value 143 | specifies the number of bytes; if it is a string, the value is passed to the 144 | [bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults 145 | to `'100kb'`. 146 | 147 | ##### type 148 | 149 | The `type` option is used to determine what media type the middleware will 150 | parse. This option can be a string, array of strings, or a function. 151 | If not a function, `type` option is passed directly to the 152 | [type-is](https://www.npmjs.org/package/type-is#readme) library and this 153 | can be an extension name (like `bin`), a mime type (like 154 | `application/octet-stream`), or a mime type with a wildcard (like `*/*` or 155 | `application/*`). If a function, the `type` option is called as `fn(req)` 156 | and the request is parsed if it returns a truthy value. Defaults to 157 | `application/octet-stream`. 158 | 159 | ##### verify 160 | 161 | The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, 162 | where `buf` is a `Buffer` of the raw request body and `encoding` is the 163 | encoding of the request. The parsing can be aborted by throwing an error. 164 | 165 | ### bodyParser.text([options]) 166 | 167 | Returns middleware that parses all bodies as a string and only looks at 168 | requests where the `Content-Type` header matches the `type` option. This 169 | parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate` 170 | encodings. 171 | 172 | A new `body` string containing the parsed data is populated on the `request` 173 | object after the middleware (i.e. `req.body`). This will be a string of the 174 | body. 175 | 176 | #### Options 177 | 178 | The `text` function takes an optional `options` object that may contain any of 179 | the following keys: 180 | 181 | ##### defaultCharset 182 | 183 | Specify the default character set for the text content if the charset is not 184 | specified in the `Content-Type` header of the request. Defaults to `utf-8`. 185 | 186 | ##### inflate 187 | 188 | When set to `true`, then deflated (compressed) bodies will be inflated; when 189 | `false`, deflated bodies are rejected. Defaults to `true`. 190 | 191 | ##### limit 192 | 193 | Controls the maximum request body size. If this is a number, then the value 194 | specifies the number of bytes; if it is a string, the value is passed to the 195 | [bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults 196 | to `'100kb'`. 197 | 198 | ##### type 199 | 200 | The `type` option is used to determine what media type the middleware will 201 | parse. This option can be a string, array of strings, or a function. If not 202 | a function, `type` option is passed directly to the 203 | [type-is](https://www.npmjs.org/package/type-is#readme) library and this can 204 | be an extension name (like `txt`), a mime type (like `text/plain`), or a mime 205 | type with a wildcard (like `*/*` or `text/*`). If a function, the `type` 206 | option is called as `fn(req)` and the request is parsed if it returns a 207 | truthy value. Defaults to `text/plain`. 208 | 209 | ##### verify 210 | 211 | The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, 212 | where `buf` is a `Buffer` of the raw request body and `encoding` is the 213 | encoding of the request. The parsing can be aborted by throwing an error. 214 | 215 | ### bodyParser.urlencoded([options]) 216 | 217 | Returns middleware that only parses `urlencoded` bodies and only looks at 218 | requests where the `Content-Type` header matches the `type` option. This 219 | parser accepts only UTF-8 encoding of the body and supports automatic 220 | inflation of `gzip`, `br` (brotli) and `deflate` encodings. 221 | 222 | A new `body` object containing the parsed data is populated on the `request` 223 | object after the middleware (i.e. `req.body`). This object will contain 224 | key-value pairs, where the value can be a string or array (when `extended` is 225 | `false`), or any type (when `extended` is `true`). 226 | 227 | #### Options 228 | 229 | The `urlencoded` function takes an optional `options` object that may contain 230 | any of the following keys: 231 | 232 | ##### extended 233 | 234 | The "extended" syntax allows for rich objects and arrays to be encoded into the 235 | URL-encoded format, allowing for a JSON-like experience with URL-encoded. For 236 | more information, please [see the qs 237 | library](https://www.npmjs.org/package/qs#readme). 238 | 239 | Defaults to `false`. 240 | 241 | ##### inflate 242 | 243 | When set to `true`, then deflated (compressed) bodies will be inflated; when 244 | `false`, deflated bodies are rejected. Defaults to `true`. 245 | 246 | ##### limit 247 | 248 | Controls the maximum request body size. If this is a number, then the value 249 | specifies the number of bytes; if it is a string, the value is passed to the 250 | [bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults 251 | to `'100kb'`. 252 | 253 | ##### parameterLimit 254 | 255 | The `parameterLimit` option controls the maximum number of parameters that 256 | are allowed in the URL-encoded data. If a request contains more parameters 257 | than this value, a 413 will be returned to the client. Defaults to `1000`. 258 | 259 | ##### type 260 | 261 | The `type` option is used to determine what media type the middleware will 262 | parse. This option can be a string, array of strings, or a function. If not 263 | a function, `type` option is passed directly to the 264 | [type-is](https://www.npmjs.org/package/type-is#readme) library and this can 265 | be an extension name (like `urlencoded`), a mime type (like 266 | `application/x-www-form-urlencoded`), or a mime type with a wildcard (like 267 | `*/x-www-form-urlencoded`). If a function, the `type` option is called as 268 | `fn(req)` and the request is parsed if it returns a truthy value. Defaults 269 | to `application/x-www-form-urlencoded`. 270 | 271 | ##### verify 272 | 273 | The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, 274 | where `buf` is a `Buffer` of the raw request body and `encoding` is the 275 | encoding of the request. The parsing can be aborted by throwing an error. 276 | 277 | ##### defaultCharset 278 | 279 | The default charset to parse as, if not specified in content-type. Must be 280 | either `utf-8` or `iso-8859-1`. Defaults to `utf-8`. 281 | 282 | ##### charsetSentinel 283 | 284 | Whether to let the value of the `utf8` parameter take precedence as the charset 285 | selector. It requires the form to contain a parameter named `utf8` with a value 286 | of `✓`. Defaults to `false`. 287 | 288 | ##### interpretNumericEntities 289 | 290 | Whether to decode numeric entities such as `☺` when parsing an iso-8859-1 291 | form. Defaults to `false`. 292 | 293 | 294 | #### depth 295 | 296 | The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible. 297 | 298 | ## Errors 299 | 300 | The middlewares provided by this module create errors using the 301 | [`http-errors` module](https://www.npmjs.com/package/http-errors). The errors 302 | will typically have a `status`/`statusCode` property that contains the suggested 303 | HTTP response code, an `expose` property to determine if the `message` property 304 | should be displayed to the client, a `type` property to determine the type of 305 | error without matching against the `message`, and a `body` property containing 306 | the read body, if available. 307 | 308 | The following are the common errors created, though any error can come through 309 | for various reasons. 310 | 311 | ### content encoding unsupported 312 | 313 | This error will occur when the request had a `Content-Encoding` header that 314 | contained an encoding but the "inflation" option was set to `false`. The 315 | `status` property is set to `415`, the `type` property is set to 316 | `'encoding.unsupported'`, and the `charset` property will be set to the 317 | encoding that is unsupported. 318 | 319 | ### entity parse failed 320 | 321 | This error will occur when the request contained an entity that could not be 322 | parsed by the middleware. The `status` property is set to `400`, the `type` 323 | property is set to `'entity.parse.failed'`, and the `body` property is set to 324 | the entity value that failed parsing. 325 | 326 | ### entity verify failed 327 | 328 | This error will occur when the request contained an entity that could not be 329 | failed verification by the defined `verify` option. The `status` property is 330 | set to `403`, the `type` property is set to `'entity.verify.failed'`, and the 331 | `body` property is set to the entity value that failed verification. 332 | 333 | ### request aborted 334 | 335 | This error will occur when the request is aborted by the client before reading 336 | the body has finished. The `received` property will be set to the number of 337 | bytes received before the request was aborted and the `expected` property is 338 | set to the number of expected bytes. The `status` property is set to `400` 339 | and `type` property is set to `'request.aborted'`. 340 | 341 | ### request entity too large 342 | 343 | This error will occur when the request body's size is larger than the "limit" 344 | option. The `limit` property will be set to the byte limit and the `length` 345 | property will be set to the request body's length. The `status` property is 346 | set to `413` and the `type` property is set to `'entity.too.large'`. 347 | 348 | ### request size did not match content length 349 | 350 | This error will occur when the request's length did not match the length from 351 | the `Content-Length` header. This typically occurs when the request is malformed, 352 | typically when the `Content-Length` header was calculated based on characters 353 | instead of bytes. The `status` property is set to `400` and the `type` property 354 | is set to `'request.size.invalid'`. 355 | 356 | ### stream encoding should not be set 357 | 358 | This error will occur when something called the `req.setEncoding` method prior 359 | to this middleware. This module operates directly on bytes only and you cannot 360 | call `req.setEncoding` when using this module. The `status` property is set to 361 | `500` and the `type` property is set to `'stream.encoding.set'`. 362 | 363 | ### stream is not readable 364 | 365 | This error will occur when the request is no longer readable when this middleware 366 | attempts to read it. This typically means something other than a middleware from 367 | this module read the request body already and the middleware was also configured to 368 | read the same request. The `status` property is set to `500` and the `type` 369 | property is set to `'stream.not.readable'`. 370 | 371 | ### too many parameters 372 | 373 | This error will occur when the content of the request exceeds the configured 374 | `parameterLimit` for the `urlencoded` parser. The `status` property is set to 375 | `413` and the `type` property is set to `'parameters.too.many'`. 376 | 377 | ### unsupported charset "BOGUS" 378 | 379 | This error will occur when the request had a charset parameter in the 380 | `Content-Type` header, but the `iconv-lite` module does not support it OR the 381 | parser does not support it. The charset is contained in the message as well 382 | as in the `charset` property. The `status` property is set to `415`, the 383 | `type` property is set to `'charset.unsupported'`, and the `charset` property 384 | is set to the charset that is unsupported. 385 | 386 | ### unsupported content encoding "bogus" 387 | 388 | This error will occur when the request had a `Content-Encoding` header that 389 | contained an unsupported encoding. The encoding is contained in the message 390 | as well as in the `encoding` property. The `status` property is set to `415`, 391 | the `type` property is set to `'encoding.unsupported'`, and the `encoding` 392 | property is set to the encoding that is unsupported. 393 | 394 | ### The input exceeded the depth 395 | 396 | This error occurs when using `bodyParser.urlencoded` with the `extended` property set to `true` and the input exceeds the configured `depth` option. The `status` property is set to `400`. It is recommended to review the `depth` option and evaluate if it requires a higher value. When the `depth` option is set to `32` (default value), the error will not be thrown. 397 | 398 | ## Examples 399 | 400 | ### Express/Connect top-level generic 401 | 402 | This example demonstrates adding a generic JSON and URL-encoded parser as a 403 | top-level middleware, which will parse the bodies of all incoming requests. 404 | This is the simplest setup. 405 | 406 | ```js 407 | const express = require('express') 408 | const bodyParser = require('body-parser') 409 | 410 | const app = express() 411 | 412 | // parse application/x-www-form-urlencoded 413 | app.use(bodyParser.urlencoded()) 414 | 415 | // parse application/json 416 | app.use(bodyParser.json()) 417 | 418 | app.use(function (req, res) { 419 | res.setHeader('Content-Type', 'text/plain') 420 | res.write('you posted:\n') 421 | res.end(String(JSON.stringify(req.body, null, 2))) 422 | }) 423 | ``` 424 | 425 | ### Express route-specific 426 | 427 | This example demonstrates adding body parsers specifically to the routes that 428 | need them. In general, this is the most recommended way to use body-parser with 429 | Express. 430 | 431 | ```js 432 | const express = require('express') 433 | const bodyParser = require('body-parser') 434 | 435 | const app = express() 436 | 437 | // create application/json parser 438 | const jsonParser = bodyParser.json() 439 | 440 | // create application/x-www-form-urlencoded parser 441 | const urlencodedParser = bodyParser.urlencoded() 442 | 443 | // POST /login gets urlencoded bodies 444 | app.post('/login', urlencodedParser, function (req, res) { 445 | if (!req.body || !req.body.username) res.sendStatus(400) 446 | res.send('welcome, ' + req.body.username) 447 | }) 448 | 449 | // POST /api/users gets JSON bodies 450 | app.post('/api/users', jsonParser, function (req, res) { 451 | if (!req.body) res.sendStatus(400) 452 | // create user in req.body 453 | }) 454 | ``` 455 | 456 | ### Change accepted type for parsers 457 | 458 | All the parsers accept a `type` option which allows you to change the 459 | `Content-Type` that the middleware will parse. 460 | 461 | ```js 462 | const express = require('express') 463 | const bodyParser = require('body-parser') 464 | 465 | const app = express() 466 | 467 | // parse various different custom JSON types as JSON 468 | app.use(bodyParser.json({ type: 'application/*+json' })) 469 | 470 | // parse some custom thing into a Buffer 471 | app.use(bodyParser.raw({ type: 'application/vnd.custom-type' })) 472 | 473 | // parse an HTML body into a string 474 | app.use(bodyParser.text({ type: 'text/html' })) 475 | ``` 476 | 477 | ## License 478 | 479 | [MIT](LICENSE) 480 | 481 | [ci-image]: https://badgen.net/github/checks/expressjs/body-parser/master?label=ci 482 | [ci-url]: https://github.com/expressjs/body-parser/actions/workflows/ci.yml 483 | [coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/body-parser/master 484 | [coveralls-url]: https://coveralls.io/r/expressjs/body-parser?branch=master 485 | [node-version-image]: https://badgen.net/npm/node/body-parser 486 | [node-version-url]: https://nodejs.org/en/download 487 | [npm-downloads-image]: https://badgen.net/npm/dm/body-parser 488 | [npm-url]: https://npmjs.org/package/body-parser 489 | [npm-version-image]: https://badgen.net/npm/v/body-parser 490 | [ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/body-parser/badge 491 | [ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/body-parser -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * body-parser 3 | * Copyright(c) 2014-2015 Douglas Christopher Wilson 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * @typedef Parsers 11 | * @type {function} 12 | * @property {function} json 13 | * @property {function} raw 14 | * @property {function} text 15 | * @property {function} urlencoded 16 | */ 17 | 18 | /** 19 | * Module exports. 20 | * @type {Parsers} 21 | */ 22 | 23 | exports = module.exports = bodyParser 24 | 25 | /** 26 | * JSON parser. 27 | * @public 28 | */ 29 | 30 | Object.defineProperty(exports, 'json', { 31 | configurable: true, 32 | enumerable: true, 33 | get: () => require('./lib/types/json') 34 | }) 35 | 36 | /** 37 | * Raw parser. 38 | * @public 39 | */ 40 | 41 | Object.defineProperty(exports, 'raw', { 42 | configurable: true, 43 | enumerable: true, 44 | get: () => require('./lib/types/raw') 45 | }) 46 | 47 | /** 48 | * Text parser. 49 | * @public 50 | */ 51 | 52 | Object.defineProperty(exports, 'text', { 53 | configurable: true, 54 | enumerable: true, 55 | get: () => require('./lib/types/text') 56 | }) 57 | 58 | /** 59 | * URL-encoded parser. 60 | * @public 61 | */ 62 | 63 | Object.defineProperty(exports, 'urlencoded', { 64 | configurable: true, 65 | enumerable: true, 66 | get: () => require('./lib/types/urlencoded') 67 | }) 68 | 69 | /** 70 | * Create a middleware to parse json and urlencoded bodies. 71 | * 72 | * @param {object} [options] 73 | * @return {function} 74 | * @deprecated 75 | * @public 76 | */ 77 | 78 | function bodyParser () { 79 | throw new Error('The bodyParser() generic has been split into individual middleware to use instead.') 80 | } 81 | -------------------------------------------------------------------------------- /lib/read.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * body-parser 3 | * Copyright(c) 2014-2015 Douglas Christopher Wilson 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * Module dependencies. 11 | * @private 12 | */ 13 | 14 | var createError = require('http-errors') 15 | var getBody = require('raw-body') 16 | var iconv = require('iconv-lite') 17 | var onFinished = require('on-finished') 18 | var zlib = require('node:zlib') 19 | 20 | /** 21 | * Module exports. 22 | */ 23 | 24 | module.exports = read 25 | 26 | /** 27 | * Read a request into a buffer and parse. 28 | * 29 | * @param {object} req 30 | * @param {object} res 31 | * @param {function} next 32 | * @param {function} parse 33 | * @param {function} debug 34 | * @param {object} options 35 | * @private 36 | */ 37 | 38 | function read (req, res, next, parse, debug, options) { 39 | var length 40 | var opts = options 41 | var stream 42 | 43 | // read options 44 | var encoding = opts.encoding !== null 45 | ? opts.encoding 46 | : null 47 | var verify = opts.verify 48 | 49 | try { 50 | // get the content stream 51 | stream = contentstream(req, debug, opts.inflate) 52 | length = stream.length 53 | stream.length = undefined 54 | } catch (err) { 55 | return next(err) 56 | } 57 | 58 | // set raw-body options 59 | opts.length = length 60 | opts.encoding = verify 61 | ? null 62 | : encoding 63 | 64 | // assert charset is supported 65 | if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) { 66 | return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', { 67 | charset: encoding.toLowerCase(), 68 | type: 'charset.unsupported' 69 | })) 70 | } 71 | 72 | // read body 73 | debug('read body') 74 | getBody(stream, opts, function (error, body) { 75 | if (error) { 76 | var _error 77 | 78 | if (error.type === 'encoding.unsupported') { 79 | // echo back charset 80 | _error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', { 81 | charset: encoding.toLowerCase(), 82 | type: 'charset.unsupported' 83 | }) 84 | } else { 85 | // set status code on error 86 | _error = createError(400, error) 87 | } 88 | 89 | // unpipe from stream and destroy 90 | if (stream !== req) { 91 | req.unpipe() 92 | stream.destroy() 93 | } 94 | 95 | // read off entire request 96 | dump(req, function onfinished () { 97 | next(createError(400, _error)) 98 | }) 99 | return 100 | } 101 | 102 | // verify 103 | if (verify) { 104 | try { 105 | debug('verify body') 106 | verify(req, res, body, encoding) 107 | } catch (err) { 108 | next(createError(403, err, { 109 | body: body, 110 | type: err.type || 'entity.verify.failed' 111 | })) 112 | return 113 | } 114 | } 115 | 116 | // parse 117 | var str = body 118 | try { 119 | debug('parse body') 120 | str = typeof body !== 'string' && encoding !== null 121 | ? iconv.decode(body, encoding) 122 | : body 123 | req.body = parse(str, encoding) 124 | } catch (err) { 125 | next(createError(400, err, { 126 | body: str, 127 | type: err.type || 'entity.parse.failed' 128 | })) 129 | return 130 | } 131 | 132 | next() 133 | }) 134 | } 135 | 136 | /** 137 | * Get the content stream of the request. 138 | * 139 | * @param {object} req 140 | * @param {function} debug 141 | * @param {boolean} [inflate=true] 142 | * @return {object} 143 | * @api private 144 | */ 145 | 146 | function contentstream (req, debug, inflate) { 147 | var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase() 148 | var length = req.headers['content-length'] 149 | 150 | debug('content-encoding "%s"', encoding) 151 | 152 | if (inflate === false && encoding !== 'identity') { 153 | throw createError(415, 'content encoding unsupported', { 154 | encoding: encoding, 155 | type: 'encoding.unsupported' 156 | }) 157 | } 158 | 159 | if (encoding === 'identity') { 160 | req.length = length 161 | return req 162 | } 163 | 164 | var stream = createDecompressionStream(encoding, debug) 165 | req.pipe(stream) 166 | return stream 167 | } 168 | 169 | /** 170 | * Create a decompression stream for the given encoding. 171 | * @param {string} encoding 172 | * @param {function} debug 173 | * @return {object} 174 | * @api private 175 | */ 176 | function createDecompressionStream (encoding, debug) { 177 | switch (encoding) { 178 | case 'deflate': 179 | debug('inflate body') 180 | return zlib.createInflate() 181 | case 'gzip': 182 | debug('gunzip body') 183 | return zlib.createGunzip() 184 | case 'br': 185 | debug('brotli decompress body') 186 | return zlib.createBrotliDecompress() 187 | default: 188 | throw createError(415, 'unsupported content encoding "' + encoding + '"', { 189 | encoding: encoding, 190 | type: 'encoding.unsupported' 191 | }) 192 | } 193 | } 194 | 195 | /** 196 | * Dump the contents of a request. 197 | * 198 | * @param {object} req 199 | * @param {function} callback 200 | * @api private 201 | */ 202 | 203 | function dump (req, callback) { 204 | if (onFinished.isFinished(req)) { 205 | callback(null) 206 | } else { 207 | onFinished(req, callback) 208 | req.resume() 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /lib/types/json.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * body-parser 3 | * Copyright(c) 2014 Jonathan Ong 4 | * Copyright(c) 2014-2015 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module dependencies. 12 | * @private 13 | */ 14 | 15 | var createError = require('http-errors') 16 | var debug = require('debug')('body-parser:json') 17 | var isFinished = require('on-finished').isFinished 18 | var read = require('../read') 19 | var typeis = require('type-is') 20 | var { getCharset, normalizeOptions } = require('../utils') 21 | 22 | /** 23 | * Module exports. 24 | */ 25 | 26 | module.exports = json 27 | 28 | /** 29 | * RegExp to match the first non-space in a string. 30 | * 31 | * Allowed whitespace is defined in RFC 7159: 32 | * 33 | * ws = *( 34 | * %x20 / ; Space 35 | * %x09 / ; Horizontal tab 36 | * %x0A / ; Line feed or New line 37 | * %x0D ) ; Carriage return 38 | */ 39 | 40 | var FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/ // eslint-disable-line no-control-regex 41 | 42 | var JSON_SYNTAX_CHAR = '#' 43 | var JSON_SYNTAX_REGEXP = /#+/g 44 | 45 | /** 46 | * Create a middleware to parse JSON bodies. 47 | * 48 | * @param {object} [options] 49 | * @return {function} 50 | * @public 51 | */ 52 | 53 | function json (options) { 54 | var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/json') 55 | 56 | var reviver = options?.reviver 57 | var strict = options?.strict !== false 58 | 59 | function parse (body) { 60 | if (body.length === 0) { 61 | // special-case empty json body, as it's a common client-side mistake 62 | // TODO: maybe make this configurable or part of "strict" option 63 | return {} 64 | } 65 | 66 | if (strict) { 67 | var first = firstchar(body) 68 | 69 | if (first !== '{' && first !== '[') { 70 | debug('strict violation') 71 | throw createStrictSyntaxError(body, first) 72 | } 73 | } 74 | 75 | try { 76 | debug('parse json') 77 | return JSON.parse(body, reviver) 78 | } catch (e) { 79 | throw normalizeJsonSyntaxError(e, { 80 | message: e.message, 81 | stack: e.stack 82 | }) 83 | } 84 | } 85 | 86 | return function jsonParser (req, res, next) { 87 | if (isFinished(req)) { 88 | debug('body already parsed') 89 | next() 90 | return 91 | } 92 | 93 | if (!('body' in req)) { 94 | req.body = undefined 95 | } 96 | 97 | // skip requests without bodies 98 | if (!typeis.hasBody(req)) { 99 | debug('skip empty body') 100 | next() 101 | return 102 | } 103 | 104 | debug('content-type %j', req.headers['content-type']) 105 | 106 | // determine if request should be parsed 107 | if (!shouldParse(req)) { 108 | debug('skip parsing') 109 | next() 110 | return 111 | } 112 | 113 | // assert charset per RFC 7159 sec 8.1 114 | var charset = getCharset(req) || 'utf-8' 115 | if (charset.slice(0, 4) !== 'utf-') { 116 | debug('invalid charset') 117 | next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', { 118 | charset: charset, 119 | type: 'charset.unsupported' 120 | })) 121 | return 122 | } 123 | 124 | // read 125 | read(req, res, next, parse, debug, { 126 | encoding: charset, 127 | inflate, 128 | limit, 129 | verify 130 | }) 131 | } 132 | } 133 | 134 | /** 135 | * Create strict violation syntax error matching native error. 136 | * 137 | * @param {string} str 138 | * @param {string} char 139 | * @return {Error} 140 | * @private 141 | */ 142 | 143 | function createStrictSyntaxError (str, char) { 144 | var index = str.indexOf(char) 145 | var partial = '' 146 | 147 | if (index !== -1) { 148 | partial = str.substring(0, index) + JSON_SYNTAX_CHAR 149 | 150 | for (var i = index + 1; i < str.length; i++) { 151 | partial += JSON_SYNTAX_CHAR 152 | } 153 | } 154 | 155 | try { 156 | JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation') 157 | } catch (e) { 158 | return normalizeJsonSyntaxError(e, { 159 | message: e.message.replace(JSON_SYNTAX_REGEXP, function (placeholder) { 160 | return str.substring(index, index + placeholder.length) 161 | }), 162 | stack: e.stack 163 | }) 164 | } 165 | } 166 | 167 | /** 168 | * Get the first non-whitespace character in a string. 169 | * 170 | * @param {string} str 171 | * @return {function} 172 | * @private 173 | */ 174 | 175 | function firstchar (str) { 176 | var match = FIRST_CHAR_REGEXP.exec(str) 177 | 178 | return match 179 | ? match[1] 180 | : undefined 181 | } 182 | 183 | /** 184 | * Normalize a SyntaxError for JSON.parse. 185 | * 186 | * @param {SyntaxError} error 187 | * @param {object} obj 188 | * @return {SyntaxError} 189 | */ 190 | 191 | function normalizeJsonSyntaxError (error, obj) { 192 | var keys = Object.getOwnPropertyNames(error) 193 | 194 | for (var i = 0; i < keys.length; i++) { 195 | var key = keys[i] 196 | if (key !== 'stack' && key !== 'message') { 197 | delete error[key] 198 | } 199 | } 200 | 201 | // replace stack before message for Node.js 0.10 and below 202 | error.stack = obj.stack.replace(error.message, obj.message) 203 | error.message = obj.message 204 | 205 | return error 206 | } 207 | -------------------------------------------------------------------------------- /lib/types/raw.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * body-parser 3 | * Copyright(c) 2014-2015 Douglas Christopher Wilson 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var debug = require('debug')('body-parser:raw') 14 | var isFinished = require('on-finished').isFinished 15 | var read = require('../read') 16 | var typeis = require('type-is') 17 | var { normalizeOptions } = require('../utils') 18 | 19 | /** 20 | * Module exports. 21 | */ 22 | 23 | module.exports = raw 24 | 25 | /** 26 | * Create a middleware to parse raw bodies. 27 | * 28 | * @param {object} [options] 29 | * @return {function} 30 | * @api public 31 | */ 32 | 33 | function raw (options) { 34 | var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/octet-stream') 35 | 36 | function parse (buf) { 37 | return buf 38 | } 39 | 40 | return function rawParser (req, res, next) { 41 | if (isFinished(req)) { 42 | debug('body already parsed') 43 | next() 44 | return 45 | } 46 | 47 | if (!('body' in req)) { 48 | req.body = undefined 49 | } 50 | 51 | // skip requests without bodies 52 | if (!typeis.hasBody(req)) { 53 | debug('skip empty body') 54 | next() 55 | return 56 | } 57 | 58 | debug('content-type %j', req.headers['content-type']) 59 | 60 | // determine if request should be parsed 61 | if (!shouldParse(req)) { 62 | debug('skip parsing') 63 | next() 64 | return 65 | } 66 | 67 | // read 68 | read(req, res, next, parse, debug, { 69 | encoding: null, 70 | inflate, 71 | limit, 72 | verify 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/types/text.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * body-parser 3 | * Copyright(c) 2014-2015 Douglas Christopher Wilson 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var debug = require('debug')('body-parser:text') 14 | var isFinished = require('on-finished').isFinished 15 | var read = require('../read') 16 | var typeis = require('type-is') 17 | var { getCharset, normalizeOptions } = require('../utils') 18 | 19 | /** 20 | * Module exports. 21 | */ 22 | 23 | module.exports = text 24 | 25 | /** 26 | * Create a middleware to parse text bodies. 27 | * 28 | * @param {object} [options] 29 | * @return {function} 30 | * @api public 31 | */ 32 | 33 | function text (options) { 34 | var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'text/plain') 35 | 36 | var defaultCharset = options?.defaultCharset || 'utf-8' 37 | 38 | function parse (buf) { 39 | return buf 40 | } 41 | 42 | return function textParser (req, res, next) { 43 | if (isFinished(req)) { 44 | debug('body already parsed') 45 | next() 46 | return 47 | } 48 | 49 | if (!('body' in req)) { 50 | req.body = undefined 51 | } 52 | 53 | // skip requests without bodies 54 | if (!typeis.hasBody(req)) { 55 | debug('skip empty body') 56 | next() 57 | return 58 | } 59 | 60 | debug('content-type %j', req.headers['content-type']) 61 | 62 | // determine if request should be parsed 63 | if (!shouldParse(req)) { 64 | debug('skip parsing') 65 | next() 66 | return 67 | } 68 | 69 | // get charset 70 | var charset = getCharset(req) || defaultCharset 71 | 72 | // read 73 | read(req, res, next, parse, debug, { 74 | encoding: charset, 75 | inflate, 76 | limit, 77 | verify 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/types/urlencoded.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * body-parser 3 | * Copyright(c) 2014 Jonathan Ong 4 | * Copyright(c) 2014-2015 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module dependencies. 12 | * @private 13 | */ 14 | 15 | var createError = require('http-errors') 16 | var debug = require('debug')('body-parser:urlencoded') 17 | var isFinished = require('on-finished').isFinished 18 | var read = require('../read') 19 | var typeis = require('type-is') 20 | var qs = require('qs') 21 | var { getCharset, normalizeOptions } = require('../utils') 22 | 23 | /** 24 | * Module exports. 25 | */ 26 | 27 | module.exports = urlencoded 28 | 29 | /** 30 | * Create a middleware to parse urlencoded bodies. 31 | * 32 | * @param {object} [options] 33 | * @return {function} 34 | * @public 35 | */ 36 | 37 | function urlencoded (options) { 38 | var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/x-www-form-urlencoded') 39 | 40 | var defaultCharset = options?.defaultCharset || 'utf-8' 41 | if (defaultCharset !== 'utf-8' && defaultCharset !== 'iso-8859-1') { 42 | throw new TypeError('option defaultCharset must be either utf-8 or iso-8859-1') 43 | } 44 | 45 | // create the appropriate query parser 46 | var queryparse = createQueryParser(options) 47 | 48 | function parse (body, encoding) { 49 | return body.length 50 | ? queryparse(body, encoding) 51 | : {} 52 | } 53 | 54 | return function urlencodedParser (req, res, next) { 55 | if (isFinished(req)) { 56 | debug('body already parsed') 57 | next() 58 | return 59 | } 60 | 61 | if (!('body' in req)) { 62 | req.body = undefined 63 | } 64 | 65 | // skip requests without bodies 66 | if (!typeis.hasBody(req)) { 67 | debug('skip empty body') 68 | next() 69 | return 70 | } 71 | 72 | debug('content-type %j', req.headers['content-type']) 73 | 74 | // determine if request should be parsed 75 | if (!shouldParse(req)) { 76 | debug('skip parsing') 77 | next() 78 | return 79 | } 80 | 81 | // assert charset 82 | var charset = getCharset(req) || defaultCharset 83 | if (charset !== 'utf-8' && charset !== 'iso-8859-1') { 84 | debug('invalid charset') 85 | next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', { 86 | charset: charset, 87 | type: 'charset.unsupported' 88 | })) 89 | return 90 | } 91 | 92 | // read 93 | read(req, res, next, parse, debug, { 94 | encoding: charset, 95 | inflate, 96 | limit, 97 | verify 98 | }) 99 | } 100 | } 101 | 102 | /** 103 | * Get the extended query parser. 104 | * 105 | * @param {object} options 106 | */ 107 | 108 | function createQueryParser (options) { 109 | var extended = Boolean(options?.extended) 110 | var parameterLimit = options?.parameterLimit !== undefined 111 | ? options?.parameterLimit 112 | : 1000 113 | var charsetSentinel = options?.charsetSentinel 114 | var interpretNumericEntities = options?.interpretNumericEntities 115 | var depth = extended ? (options?.depth !== undefined ? options?.depth : 32) : 0 116 | 117 | if (isNaN(parameterLimit) || parameterLimit < 1) { 118 | throw new TypeError('option parameterLimit must be a positive number') 119 | } 120 | 121 | if (isNaN(depth) || depth < 0) { 122 | throw new TypeError('option depth must be a zero or a positive number') 123 | } 124 | 125 | if (isFinite(parameterLimit)) { 126 | parameterLimit = parameterLimit | 0 127 | } 128 | 129 | return function queryparse (body, encoding) { 130 | var paramCount = parameterCount(body, parameterLimit) 131 | 132 | if (paramCount === undefined) { 133 | debug('too many parameters') 134 | throw createError(413, 'too many parameters', { 135 | type: 'parameters.too.many' 136 | }) 137 | } 138 | 139 | var arrayLimit = extended ? Math.max(100, paramCount) : 0 140 | 141 | debug('parse ' + (extended ? 'extended ' : '') + 'urlencoding') 142 | try { 143 | return qs.parse(body, { 144 | allowPrototypes: true, 145 | arrayLimit: arrayLimit, 146 | depth: depth, 147 | charsetSentinel: charsetSentinel, 148 | interpretNumericEntities: interpretNumericEntities, 149 | charset: encoding, 150 | parameterLimit: parameterLimit, 151 | strictDepth: true 152 | }) 153 | } catch (err) { 154 | if (err instanceof RangeError) { 155 | throw createError(400, 'The input exceeded the depth', { 156 | type: 'querystring.parse.rangeError' 157 | }) 158 | } else { 159 | throw err 160 | } 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Count the number of parameters, stopping once limit reached 167 | * 168 | * @param {string} body 169 | * @param {number} limit 170 | * @api private 171 | */ 172 | 173 | function parameterCount (body, limit) { 174 | var len = body.split('&').length 175 | 176 | return len > limit ? undefined : len - 1 177 | } 178 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var bytes = require('bytes') 8 | var contentType = require('content-type') 9 | var typeis = require('type-is') 10 | 11 | /** 12 | * Module exports. 13 | */ 14 | 15 | module.exports = { 16 | getCharset, 17 | normalizeOptions 18 | } 19 | 20 | /** 21 | * Get the charset of a request. 22 | * 23 | * @param {object} req 24 | * @api private 25 | */ 26 | 27 | function getCharset (req) { 28 | try { 29 | return (contentType.parse(req).parameters.charset || '').toLowerCase() 30 | } catch { 31 | return undefined 32 | } 33 | } 34 | 35 | /** 36 | * Get the simple type checker. 37 | * 38 | * @param {string | string[]} type 39 | * @return {function} 40 | */ 41 | 42 | function typeChecker (type) { 43 | return function checkType (req) { 44 | return Boolean(typeis(req, type)) 45 | } 46 | } 47 | 48 | /** 49 | * Normalizes the common options for all parsers. 50 | * 51 | * @param {object} options options to normalize 52 | * @param {string | string[] | function} defaultType default content type(s) or a function to determine it 53 | * @returns {object} 54 | */ 55 | function normalizeOptions (options, defaultType) { 56 | if (!defaultType) { 57 | // Parsers must define a default content type 58 | throw new TypeError('defaultType must be provided') 59 | } 60 | 61 | var inflate = options?.inflate !== false 62 | var limit = typeof options?.limit !== 'number' 63 | ? bytes.parse(options?.limit || '100kb') 64 | : options?.limit 65 | var type = options?.type || defaultType 66 | var verify = options?.verify || false 67 | 68 | if (verify !== false && typeof verify !== 'function') { 69 | throw new TypeError('option verify must be function') 70 | } 71 | 72 | // create the appropriate type checking function 73 | var shouldParse = typeof type !== 'function' 74 | ? typeChecker(type) 75 | : type 76 | 77 | return { 78 | inflate, 79 | limit, 80 | verify, 81 | shouldParse 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "body-parser", 3 | "description": "Node.js body parsing middleware", 4 | "version": "2.2.0", 5 | "contributors": [ 6 | "Douglas Christopher Wilson ", 7 | "Jonathan Ong (http://jongleberry.com)" 8 | ], 9 | "license": "MIT", 10 | "repository": "expressjs/body-parser", 11 | "dependencies": { 12 | "bytes": "^3.1.2", 13 | "content-type": "^1.0.5", 14 | "debug": "^4.4.0", 15 | "http-errors": "^2.0.0", 16 | "iconv-lite": "^0.6.3", 17 | "on-finished": "^2.4.1", 18 | "qs": "^6.14.0", 19 | "raw-body": "^3.0.0", 20 | "type-is": "^2.0.1" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^8.57.1", 24 | "eslint-config-standard": "^14.1.1", 25 | "eslint-plugin-import": "^2.31.0", 26 | "eslint-plugin-markdown": "^3.0.1", 27 | "eslint-plugin-node": "^11.1.0", 28 | "eslint-plugin-promise": "^6.6.0", 29 | "eslint-plugin-standard": "^4.1.0", 30 | "mocha": "^11.1.0", 31 | "nyc": "^17.1.0", 32 | "supertest": "^7.0.0" 33 | }, 34 | "files": [ 35 | "lib/", 36 | "LICENSE", 37 | "HISTORY.md", 38 | "index.js" 39 | ], 40 | "engines": { 41 | "node": ">=18" 42 | }, 43 | "scripts": { 44 | "lint": "eslint .", 45 | "test": "mocha --reporter spec --check-leaks test/", 46 | "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", 47 | "test-cov": "nyc --reporter=html --reporter=text npm test" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /test/body-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('node:assert') 4 | 5 | var bodyParser = require('..') 6 | 7 | describe('bodyParser()', function () { 8 | it('should throw an error', function () { 9 | assert.throws(bodyParser, /bodyParser\(\) generic has been split/) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/json.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('node:assert') 4 | var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage 5 | var http = require('node:http') 6 | var request = require('supertest') 7 | 8 | var bodyParser = require('..') 9 | 10 | describe('bodyParser.json()', function () { 11 | it('should parse JSON', function (done) { 12 | request(createServer()) 13 | .post('/') 14 | .set('Content-Type', 'application/json') 15 | .send('{"user":"tobi"}') 16 | .expect(200, '{"user":"tobi"}', done) 17 | }) 18 | 19 | it('should handle Content-Length: 0', function (done) { 20 | request(createServer()) 21 | .get('/') 22 | .set('Content-Type', 'application/json') 23 | .set('Content-Length', '0') 24 | .expect(200, '{}', done) 25 | }) 26 | 27 | it('should handle empty message-body', function (done) { 28 | request(createServer()) 29 | .get('/') 30 | .set('Content-Type', 'application/json') 31 | .set('Transfer-Encoding', 'chunked') 32 | .expect(200, '{}', done) 33 | }) 34 | 35 | it('should handle no message-body', function (done) { 36 | request(createServer()) 37 | .get('/') 38 | .set('Content-Type', 'application/json') 39 | .unset('Transfer-Encoding') 40 | .expect(200, 'undefined', done) 41 | }) 42 | 43 | it('should 400 when only whitespace', function (done) { 44 | request(createServer()) 45 | .post('/') 46 | .set('Content-Type', 'application/json') 47 | .send(' \n') 48 | .expect(400, '[entity.parse.failed] ' + parseError(' '), done) 49 | }) 50 | 51 | it('should 400 when invalid content-length', function (done) { 52 | var jsonParser = bodyParser.json() 53 | var server = createServer(function (req, res, next) { 54 | req.headers['content-length'] = '20' // bad length 55 | jsonParser(req, res, next) 56 | }) 57 | 58 | request(server) 59 | .post('/') 60 | .set('Content-Type', 'application/json') 61 | .send('{"str":') 62 | .expect(400, /content length/, done) 63 | }) 64 | 65 | it('should handle consumed request', function (done) { 66 | var jsonParser = bodyParser.json() 67 | var server = createServer(function (req, res, next) { 68 | req.on('end', function () { 69 | jsonParser(req, res, next) 70 | }) 71 | req.resume() 72 | }) 73 | 74 | request(server) 75 | .post('/') 76 | .set('Content-Type', 'application/json') 77 | .send('{"user":"tobi"}') 78 | .expect(200, 'undefined', done) 79 | }) 80 | 81 | it('should handle duplicated middleware', function (done) { 82 | var jsonParser = bodyParser.json() 83 | var server = createServer(function (req, res, next) { 84 | jsonParser(req, res, function (err) { 85 | if (err) return next(err) 86 | jsonParser(req, res, next) 87 | }) 88 | }) 89 | 90 | request(server) 91 | .post('/') 92 | .set('Content-Type', 'application/json') 93 | .send('{"user":"tobi"}') 94 | .expect(200, '{"user":"tobi"}', done) 95 | }) 96 | 97 | describe('when JSON is invalid', function () { 98 | before(function () { 99 | this.server = createServer() 100 | }) 101 | 102 | it('should 400 for bad token', function (done) { 103 | request(this.server) 104 | .post('/') 105 | .set('Content-Type', 'application/json') 106 | .send('{:') 107 | .expect(400, '[entity.parse.failed] ' + parseError('{:'), done) 108 | }) 109 | 110 | it('should 400 for incomplete', function (done) { 111 | request(this.server) 112 | .post('/') 113 | .set('Content-Type', 'application/json') 114 | .send('{"user"') 115 | .expect(400, '[entity.parse.failed] ' + parseError('{"user"'), done) 116 | }) 117 | 118 | it('should include original body on error object', function (done) { 119 | request(this.server) 120 | .post('/') 121 | .set('Content-Type', 'application/json') 122 | .set('X-Error-Property', 'body') 123 | .send(' {"user"') 124 | .expect(400, ' {"user"', done) 125 | }) 126 | }) 127 | 128 | describe('with limit option', function () { 129 | it('should 413 when over limit with Content-Length', function (done) { 130 | var buf = Buffer.alloc(1024, '.') 131 | request(createServer({ limit: '1kb' })) 132 | .post('/') 133 | .set('Content-Type', 'application/json') 134 | .set('Content-Length', '1034') 135 | .send(JSON.stringify({ str: buf.toString() })) 136 | .expect(413, '[entity.too.large] request entity too large', done) 137 | }) 138 | 139 | it('should 413 when over limit with chunked encoding', function (done) { 140 | var buf = Buffer.alloc(1024, '.') 141 | var server = createServer({ limit: '1kb' }) 142 | var test = request(server).post('/') 143 | test.set('Content-Type', 'application/json') 144 | test.set('Transfer-Encoding', 'chunked') 145 | test.write('{"str":') 146 | test.write('"' + buf.toString() + '"}') 147 | test.expect(413, done) 148 | }) 149 | 150 | it('should 413 when inflated body over limit', function (done) { 151 | var server = createServer({ limit: '1kb' }) 152 | var test = request(server).post('/') 153 | test.set('Content-Encoding', 'gzip') 154 | test.set('Content-Type', 'application/json') 155 | test.write(Buffer.from('1f8b080000000000000aab562a2e2952b252d21b05a360148c58a0540b0066f7ce1e0a040000', 'hex')) 156 | test.expect(413, done) 157 | }) 158 | 159 | it('should accept number of bytes', function (done) { 160 | var buf = Buffer.alloc(1024, '.') 161 | request(createServer({ limit: 1024 })) 162 | .post('/') 163 | .set('Content-Type', 'application/json') 164 | .send(JSON.stringify({ str: buf.toString() })) 165 | .expect(413, done) 166 | }) 167 | 168 | it('should not change when options altered', function (done) { 169 | var buf = Buffer.alloc(1024, '.') 170 | var options = { limit: '1kb' } 171 | var server = createServer(options) 172 | 173 | options.limit = '100kb' 174 | 175 | request(server) 176 | .post('/') 177 | .set('Content-Type', 'application/json') 178 | .send(JSON.stringify({ str: buf.toString() })) 179 | .expect(413, done) 180 | }) 181 | 182 | it('should not hang response', function (done) { 183 | var buf = Buffer.alloc(10240, '.') 184 | var server = createServer({ limit: '8kb' }) 185 | var test = request(server).post('/') 186 | test.set('Content-Type', 'application/json') 187 | test.write(buf) 188 | test.write(buf) 189 | test.write(buf) 190 | test.expect(413, done) 191 | }) 192 | 193 | it('should not error when inflating', function (done) { 194 | var server = createServer({ limit: '1kb' }) 195 | var test = request(server).post('/') 196 | test.set('Content-Encoding', 'gzip') 197 | test.set('Content-Type', 'application/json') 198 | test.write(Buffer.from('1f8b080000000000000aab562a2e2952b252d21b05a360148c58a0540b0066f7ce1e0a0400', 'hex')) 199 | test.expect(413, done) 200 | }) 201 | }) 202 | 203 | describe('with inflate option', function () { 204 | describe('when false', function () { 205 | before(function () { 206 | this.server = createServer({ inflate: false }) 207 | }) 208 | 209 | it('should not accept content-encoding', function (done) { 210 | var test = request(this.server).post('/') 211 | test.set('Content-Encoding', 'gzip') 212 | test.set('Content-Type', 'application/json') 213 | test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) 214 | test.expect(415, '[encoding.unsupported] content encoding unsupported', done) 215 | }) 216 | }) 217 | 218 | describe('when true', function () { 219 | before(function () { 220 | this.server = createServer({ inflate: true }) 221 | }) 222 | 223 | it('should accept content-encoding', function (done) { 224 | var test = request(this.server).post('/') 225 | test.set('Content-Encoding', 'gzip') 226 | test.set('Content-Type', 'application/json') 227 | test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) 228 | test.expect(200, '{"name":"论"}', done) 229 | }) 230 | }) 231 | }) 232 | 233 | describe('with strict option', function () { 234 | describe('when undefined', function () { 235 | before(function () { 236 | this.server = createServer() 237 | }) 238 | 239 | it('should 400 on primitives', function (done) { 240 | request(this.server) 241 | .post('/') 242 | .set('Content-Type', 'application/json') 243 | .send('true') 244 | .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done) 245 | }) 246 | }) 247 | 248 | describe('when false', function () { 249 | before(function () { 250 | this.server = createServer({ strict: false }) 251 | }) 252 | 253 | it('should parse primitives', function (done) { 254 | request(this.server) 255 | .post('/') 256 | .set('Content-Type', 'application/json') 257 | .send('true') 258 | .expect(200, 'true', done) 259 | }) 260 | }) 261 | 262 | describe('when true', function () { 263 | before(function () { 264 | this.server = createServer({ strict: true }) 265 | }) 266 | 267 | it('should not parse primitives', function (done) { 268 | request(this.server) 269 | .post('/') 270 | .set('Content-Type', 'application/json') 271 | .send('true') 272 | .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done) 273 | }) 274 | 275 | it('should not parse primitives with leading whitespaces', function (done) { 276 | request(this.server) 277 | .post('/') 278 | .set('Content-Type', 'application/json') 279 | .send(' true') 280 | .expect(400, '[entity.parse.failed] ' + parseError(' #rue').replace(/#/g, 't'), done) 281 | }) 282 | 283 | it('should allow leading whitespaces in JSON', function (done) { 284 | request(this.server) 285 | .post('/') 286 | .set('Content-Type', 'application/json') 287 | .send(' { "user": "tobi" }') 288 | .expect(200, '{"user":"tobi"}', done) 289 | }) 290 | 291 | it('should include correct message in stack trace', function (done) { 292 | request(this.server) 293 | .post('/') 294 | .set('Content-Type', 'application/json') 295 | .set('X-Error-Property', 'stack') 296 | .send('true') 297 | .expect(400) 298 | .expect(shouldContainInBody(parseError('#rue').replace(/#/g, 't'))) 299 | .end(done) 300 | }) 301 | }) 302 | }) 303 | 304 | describe('with type option', function () { 305 | describe('when "application/vnd.api+json"', function () { 306 | before(function () { 307 | this.server = createServer({ type: 'application/vnd.api+json' }) 308 | }) 309 | 310 | it('should parse JSON for custom type', function (done) { 311 | request(this.server) 312 | .post('/') 313 | .set('Content-Type', 'application/vnd.api+json') 314 | .send('{"user":"tobi"}') 315 | .expect(200, '{"user":"tobi"}', done) 316 | }) 317 | 318 | it('should ignore standard type', function (done) { 319 | request(this.server) 320 | .post('/') 321 | .set('Content-Type', 'application/json') 322 | .send('{"user":"tobi"}') 323 | .expect(200, 'undefined', done) 324 | }) 325 | }) 326 | 327 | describe('when ["application/json", "application/vnd.api+json"]', function () { 328 | before(function () { 329 | this.server = createServer({ 330 | type: ['application/json', 'application/vnd.api+json'] 331 | }) 332 | }) 333 | 334 | it('should parse JSON for "application/json"', function (done) { 335 | request(this.server) 336 | .post('/') 337 | .set('Content-Type', 'application/json') 338 | .send('{"user":"tobi"}') 339 | .expect(200, '{"user":"tobi"}', done) 340 | }) 341 | 342 | it('should parse JSON for "application/vnd.api+json"', function (done) { 343 | request(this.server) 344 | .post('/') 345 | .set('Content-Type', 'application/vnd.api+json') 346 | .send('{"user":"tobi"}') 347 | .expect(200, '{"user":"tobi"}', done) 348 | }) 349 | 350 | it('should ignore "application/x-json"', function (done) { 351 | request(this.server) 352 | .post('/') 353 | .set('Content-Type', 'application/x-json') 354 | .send('{"user":"tobi"}') 355 | .expect(200, 'undefined', done) 356 | }) 357 | }) 358 | 359 | describe('when a function', function () { 360 | it('should parse when truthy value returned', function (done) { 361 | var server = createServer({ type: accept }) 362 | 363 | function accept (req) { 364 | return req.headers['content-type'] === 'application/vnd.api+json' 365 | } 366 | 367 | request(server) 368 | .post('/') 369 | .set('Content-Type', 'application/vnd.api+json') 370 | .send('{"user":"tobi"}') 371 | .expect(200, '{"user":"tobi"}', done) 372 | }) 373 | 374 | it('should work without content-type', function (done) { 375 | var server = createServer({ type: accept }) 376 | 377 | function accept (req) { 378 | return true 379 | } 380 | 381 | var test = request(server).post('/') 382 | test.write('{"user":"tobi"}') 383 | test.expect(200, '{"user":"tobi"}', done) 384 | }) 385 | 386 | it('should not invoke without a body', function (done) { 387 | var server = createServer({ type: accept }) 388 | 389 | function accept (req) { 390 | throw new Error('oops!') 391 | } 392 | 393 | request(server) 394 | .get('/') 395 | .expect(200, done) 396 | }) 397 | }) 398 | }) 399 | 400 | describe('with verify option', function () { 401 | it('should assert value if function', function () { 402 | assert.throws(createServer.bind(null, { verify: 'lol' }), 403 | /TypeError: option verify must be function/) 404 | }) 405 | 406 | it('should error from verify', function (done) { 407 | var server = createServer({ 408 | verify: function (req, res, buf) { 409 | if (buf[0] === 0x5b) throw new Error('no arrays') 410 | } 411 | }) 412 | 413 | request(server) 414 | .post('/') 415 | .set('Content-Type', 'application/json') 416 | .send('["tobi"]') 417 | .expect(403, '[entity.verify.failed] no arrays', done) 418 | }) 419 | 420 | it('should allow custom codes', function (done) { 421 | var server = createServer({ 422 | verify: function (req, res, buf) { 423 | if (buf[0] !== 0x5b) return 424 | var err = new Error('no arrays') 425 | err.status = 400 426 | throw err 427 | } 428 | }) 429 | 430 | request(server) 431 | .post('/') 432 | .set('Content-Type', 'application/json') 433 | .send('["tobi"]') 434 | .expect(400, '[entity.verify.failed] no arrays', done) 435 | }) 436 | 437 | it('should allow custom type', function (done) { 438 | var server = createServer({ 439 | verify: function (req, res, buf) { 440 | if (buf[0] !== 0x5b) return 441 | var err = new Error('no arrays') 442 | err.type = 'foo.bar' 443 | throw err 444 | } 445 | }) 446 | 447 | request(server) 448 | .post('/') 449 | .set('Content-Type', 'application/json') 450 | .send('["tobi"]') 451 | .expect(403, '[foo.bar] no arrays', done) 452 | }) 453 | 454 | it('should include original body on error object', function (done) { 455 | var server = createServer({ 456 | verify: function (req, res, buf) { 457 | if (buf[0] === 0x5b) throw new Error('no arrays') 458 | } 459 | }) 460 | 461 | request(server) 462 | .post('/') 463 | .set('Content-Type', 'application/json') 464 | .set('X-Error-Property', 'body') 465 | .send('["tobi"]') 466 | .expect(403, '["tobi"]', done) 467 | }) 468 | 469 | it('should allow pass-through', function (done) { 470 | var server = createServer({ 471 | verify: function (req, res, buf) { 472 | if (buf[0] === 0x5b) throw new Error('no arrays') 473 | } 474 | }) 475 | 476 | request(server) 477 | .post('/') 478 | .set('Content-Type', 'application/json') 479 | .send('{"user":"tobi"}') 480 | .expect(200, '{"user":"tobi"}', done) 481 | }) 482 | 483 | it('should work with different charsets', function (done) { 484 | var server = createServer({ 485 | verify: function (req, res, buf) { 486 | if (buf[0] === 0x5b) throw new Error('no arrays') 487 | } 488 | }) 489 | 490 | var test = request(server).post('/') 491 | test.set('Content-Type', 'application/json; charset=utf-16') 492 | test.write(Buffer.from('feff007b0022006e0061006d00650022003a00228bba0022007d', 'hex')) 493 | test.expect(200, '{"name":"论"}', done) 494 | }) 495 | 496 | it('should 415 on unknown charset prior to verify', function (done) { 497 | var server = createServer({ 498 | verify: function (req, res, buf) { 499 | throw new Error('unexpected verify call') 500 | } 501 | }) 502 | 503 | var test = request(server).post('/') 504 | test.set('Content-Type', 'application/json; charset=x-bogus') 505 | test.write(Buffer.from('00000000', 'hex')) 506 | test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) 507 | }) 508 | }) 509 | 510 | describe('async local storage', function () { 511 | before(function () { 512 | var jsonParser = bodyParser.json() 513 | var store = { foo: 'bar' } 514 | 515 | this.server = createServer(function (req, res, next) { 516 | var asyncLocalStorage = new AsyncLocalStorage() 517 | 518 | asyncLocalStorage.run(store, function () { 519 | jsonParser(req, res, function (err) { 520 | var local = asyncLocalStorage.getStore() 521 | 522 | if (local) { 523 | res.setHeader('x-store-foo', String(local.foo)) 524 | } 525 | 526 | next(err) 527 | }) 528 | }) 529 | }) 530 | }) 531 | 532 | it('should presist store', function (done) { 533 | request(this.server) 534 | .post('/') 535 | .set('Content-Type', 'application/json') 536 | .send('{"user":"tobi"}') 537 | .expect(200) 538 | .expect('x-store-foo', 'bar') 539 | .expect('{"user":"tobi"}') 540 | .end(done) 541 | }) 542 | 543 | it('should presist store when unmatched content-type', function (done) { 544 | request(this.server) 545 | .post('/') 546 | .set('Content-Type', 'application/fizzbuzz') 547 | .send('buzz') 548 | .expect(200) 549 | .expect('x-store-foo', 'bar') 550 | .expect('undefined') 551 | .end(done) 552 | }) 553 | 554 | it('should presist store when inflated', function (done) { 555 | var test = request(this.server).post('/') 556 | test.set('Content-Encoding', 'gzip') 557 | test.set('Content-Type', 'application/json') 558 | test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) 559 | test.expect(200) 560 | test.expect('x-store-foo', 'bar') 561 | test.expect('{"name":"论"}') 562 | test.end(done) 563 | }) 564 | 565 | it('should presist store when inflate error', function (done) { 566 | var test = request(this.server).post('/') 567 | test.set('Content-Encoding', 'gzip') 568 | test.set('Content-Type', 'application/json') 569 | test.write(Buffer.from('1f8b080000000000000bab56cc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) 570 | test.expect(400) 571 | test.expect('x-store-foo', 'bar') 572 | test.end(done) 573 | }) 574 | 575 | it('should presist store when parse error', function (done) { 576 | request(this.server) 577 | .post('/') 578 | .set('Content-Type', 'application/json') 579 | .send('{"user":') 580 | .expect(400) 581 | .expect('x-store-foo', 'bar') 582 | .end(done) 583 | }) 584 | 585 | it('should presist store when limit exceeded', function (done) { 586 | request(this.server) 587 | .post('/') 588 | .set('Content-Type', 'application/json') 589 | .send('{"user":"' + Buffer.alloc(1024 * 100, '.').toString() + '"}') 590 | .expect(413) 591 | .expect('x-store-foo', 'bar') 592 | .end(done) 593 | }) 594 | }) 595 | 596 | describe('charset', function () { 597 | before(function () { 598 | this.server = createServer() 599 | }) 600 | 601 | it('should parse utf-8', function (done) { 602 | var test = request(this.server).post('/') 603 | test.set('Content-Type', 'application/json; charset=utf-8') 604 | test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) 605 | test.expect(200, '{"name":"论"}', done) 606 | }) 607 | 608 | it('should parse utf-16', function (done) { 609 | var test = request(this.server).post('/') 610 | test.set('Content-Type', 'application/json; charset=utf-16') 611 | test.write(Buffer.from('feff007b0022006e0061006d00650022003a00228bba0022007d', 'hex')) 612 | test.expect(200, '{"name":"论"}', done) 613 | }) 614 | 615 | it('should parse utf-32', function (done) { 616 | var test = request(this.server).post('/') 617 | test.set('Content-Type', 'application/json; charset=utf-32') 618 | test.write(Buffer.from('fffe00007b000000220000006e000000610000006d00000065000000220000003a00000022000000ba8b0000220000007d000000', 'hex')) 619 | test.expect(200, '{"name":"论"}', done) 620 | }) 621 | 622 | it('should parse when content-length != char length', function (done) { 623 | var test = request(this.server).post('/') 624 | test.set('Content-Type', 'application/json; charset=utf-8') 625 | test.set('Content-Length', '13') 626 | test.write(Buffer.from('7b2274657374223a22c3a5227d', 'hex')) 627 | test.expect(200, '{"test":"å"}', done) 628 | }) 629 | 630 | it('should default to utf-8', function (done) { 631 | var test = request(this.server).post('/') 632 | test.set('Content-Type', 'application/json') 633 | test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) 634 | test.expect(200, '{"name":"论"}', done) 635 | }) 636 | 637 | it('should fail on unknown charset', function (done) { 638 | var test = request(this.server).post('/') 639 | test.set('Content-Type', 'application/json; charset=koi8-r') 640 | test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex')) 641 | test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done) 642 | }) 643 | }) 644 | 645 | describe('encoding', function () { 646 | before(function () { 647 | this.server = createServer({ limit: '1kb' }) 648 | }) 649 | 650 | it('should parse without encoding', function (done) { 651 | var test = request(this.server).post('/') 652 | test.set('Content-Type', 'application/json') 653 | test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) 654 | test.expect(200, '{"name":"论"}', done) 655 | }) 656 | 657 | it('should support identity encoding', function (done) { 658 | var test = request(this.server).post('/') 659 | test.set('Content-Encoding', 'identity') 660 | test.set('Content-Type', 'application/json') 661 | test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) 662 | test.expect(200, '{"name":"论"}', done) 663 | }) 664 | 665 | it('should support gzip encoding', function (done) { 666 | var test = request(this.server).post('/') 667 | test.set('Content-Encoding', 'gzip') 668 | test.set('Content-Type', 'application/json') 669 | test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) 670 | test.expect(200, '{"name":"论"}', done) 671 | }) 672 | 673 | it('should support deflate encoding', function (done) { 674 | var test = request(this.server).post('/') 675 | test.set('Content-Encoding', 'deflate') 676 | test.set('Content-Type', 'application/json') 677 | test.write(Buffer.from('789cab56ca4bcc4d55b2527ab16e97522d00274505ac', 'hex')) 678 | test.expect(200, '{"name":"论"}', done) 679 | }) 680 | 681 | it('should support brotli encoding', function (done) { 682 | var test = request(this.server).post('/') 683 | test.set('Content-Encoding', 'br') 684 | test.set('Content-Type', 'application/json') 685 | test.write(Buffer.from('8b06807b226e616d65223a22e8aeba227d03', 'hex')) 686 | test.expect(200, '{"name":"论"}', done) 687 | }) 688 | 689 | it('should be case-insensitive', function (done) { 690 | var test = request(this.server).post('/') 691 | test.set('Content-Encoding', 'GZIP') 692 | test.set('Content-Type', 'application/json') 693 | test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) 694 | test.expect(200, '{"name":"论"}', done) 695 | }) 696 | 697 | it('should 415 on unknown encoding', function (done) { 698 | var test = request(this.server).post('/') 699 | test.set('Content-Encoding', 'nulls') 700 | test.set('Content-Type', 'application/json') 701 | test.write(Buffer.from('000000000000', 'hex')) 702 | test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) 703 | }) 704 | 705 | it('should 400 on malformed encoding', function (done) { 706 | var test = request(this.server).post('/') 707 | test.set('Content-Encoding', 'gzip') 708 | test.set('Content-Type', 'application/json') 709 | test.write(Buffer.from('1f8b080000000000000bab56cc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) 710 | test.expect(400, done) 711 | }) 712 | 713 | it('should 413 when inflated value exceeds limit', function (done) { 714 | // gzip'd data exceeds 1kb, but deflated below 1kb 715 | var test = request(this.server).post('/') 716 | test.set('Content-Encoding', 'gzip') 717 | test.set('Content-Type', 'application/json') 718 | test.write(Buffer.from('1f8b080000000000000bedc1010d000000c2a0f74f6d0f071400000000000000', 'hex')) 719 | test.write(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex')) 720 | test.write(Buffer.from('0000000000000000004f0625b3b71650c30000', 'hex')) 721 | test.expect(413, done) 722 | }) 723 | }) 724 | }) 725 | 726 | function createServer (opts) { 727 | var _bodyParser = typeof opts !== 'function' 728 | ? bodyParser.json(opts) 729 | : opts 730 | 731 | return http.createServer(function (req, res) { 732 | _bodyParser(req, res, function (err) { 733 | if (err) { 734 | res.statusCode = err.status || 500 735 | res.end(req.headers['x-error-property'] 736 | ? err[req.headers['x-error-property']] 737 | : ('[' + err.type + '] ' + err.message)) 738 | } else { 739 | res.statusCode = 200 740 | res.end(JSON.stringify(req.body) || typeof req.body) 741 | } 742 | }) 743 | }) 744 | } 745 | 746 | function parseError (str) { 747 | try { 748 | JSON.parse(str); throw new SyntaxError('strict violation') 749 | } catch (e) { 750 | return e.message 751 | } 752 | } 753 | 754 | function shouldContainInBody (str) { 755 | return function (res) { 756 | assert.ok(res.text.indexOf(str) !== -1, 757 | 'expected \'' + res.text + '\' to contain \'' + str + '\'') 758 | } 759 | } 760 | -------------------------------------------------------------------------------- /test/raw.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('node:assert') 4 | var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage 5 | var http = require('node:http') 6 | var request = require('supertest') 7 | 8 | var bodyParser = require('..') 9 | 10 | describe('bodyParser.raw()', function () { 11 | before(function () { 12 | this.server = createServer() 13 | }) 14 | 15 | it('should parse application/octet-stream', function (done) { 16 | request(this.server) 17 | .post('/') 18 | .set('Content-Type', 'application/octet-stream') 19 | .send('the user is tobi') 20 | .expect(200, 'buf:746865207573657220697320746f6269', done) 21 | }) 22 | 23 | it('should 400 when invalid content-length', function (done) { 24 | var rawParser = bodyParser.raw() 25 | var server = createServer(function (req, res, next) { 26 | req.headers['content-length'] = '20' // bad length 27 | rawParser(req, res, next) 28 | }) 29 | 30 | request(server) 31 | .post('/') 32 | .set('Content-Type', 'application/octet-stream') 33 | .send('stuff') 34 | .expect(400, /content length/, done) 35 | }) 36 | 37 | it('should handle Content-Length: 0', function (done) { 38 | request(this.server) 39 | .post('/') 40 | .set('Content-Type', 'application/octet-stream') 41 | .set('Content-Length', '0') 42 | .expect(200, 'buf:', done) 43 | }) 44 | 45 | it('should handle empty message-body', function (done) { 46 | request(this.server) 47 | .post('/') 48 | .set('Content-Type', 'application/octet-stream') 49 | .set('Transfer-Encoding', 'chunked') 50 | .send('') 51 | .expect(200, 'buf:', done) 52 | }) 53 | 54 | it('should handle consumed stream', function (done) { 55 | var rawParser = bodyParser.raw() 56 | var server = createServer(function (req, res, next) { 57 | req.on('end', function () { 58 | rawParser(req, res, next) 59 | }) 60 | req.resume() 61 | }) 62 | 63 | request(server) 64 | .post('/') 65 | .set('Content-Type', 'application/octet-stream') 66 | .send('the user is tobi') 67 | .expect(200, 'undefined', done) 68 | }) 69 | 70 | it('should handle duplicated middleware', function (done) { 71 | var rawParser = bodyParser.raw() 72 | var server = createServer(function (req, res, next) { 73 | rawParser(req, res, function (err) { 74 | if (err) return next(err) 75 | rawParser(req, res, next) 76 | }) 77 | }) 78 | 79 | request(server) 80 | .post('/') 81 | .set('Content-Type', 'application/octet-stream') 82 | .send('the user is tobi') 83 | .expect(200, 'buf:746865207573657220697320746f6269', done) 84 | }) 85 | 86 | describe('with limit option', function () { 87 | it('should 413 when over limit with Content-Length', function (done) { 88 | var buf = Buffer.alloc(1028, '.') 89 | var server = createServer({ limit: '1kb' }) 90 | var test = request(server).post('/') 91 | test.set('Content-Type', 'application/octet-stream') 92 | test.set('Content-Length', '1028') 93 | test.write(buf) 94 | test.expect(413, done) 95 | }) 96 | 97 | it('should 413 when over limit with chunked encoding', function (done) { 98 | var buf = Buffer.alloc(1028, '.') 99 | var server = createServer({ limit: '1kb' }) 100 | var test = request(server).post('/') 101 | test.set('Content-Type', 'application/octet-stream') 102 | test.set('Transfer-Encoding', 'chunked') 103 | test.write(buf) 104 | test.expect(413, done) 105 | }) 106 | 107 | it('should 413 when inflated body over limit', function (done) { 108 | var server = createServer({ limit: '1kb' }) 109 | var test = request(server).post('/') 110 | test.set('Content-Encoding', 'gzip') 111 | test.set('Content-Type', 'application/octet-stream') 112 | test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a14704040000', 'hex')) 113 | test.expect(413, done) 114 | }) 115 | 116 | it('should accept number of bytes', function (done) { 117 | var buf = Buffer.alloc(1028, '.') 118 | var server = createServer({ limit: 1024 }) 119 | var test = request(server).post('/') 120 | test.set('Content-Type', 'application/octet-stream') 121 | test.write(buf) 122 | test.expect(413, done) 123 | }) 124 | 125 | it('should not change when options altered', function (done) { 126 | var buf = Buffer.alloc(1028, '.') 127 | var options = { limit: '1kb' } 128 | var server = createServer(options) 129 | 130 | options.limit = '100kb' 131 | 132 | var test = request(server).post('/') 133 | test.set('Content-Type', 'application/octet-stream') 134 | test.write(buf) 135 | test.expect(413, done) 136 | }) 137 | 138 | it('should not hang response', function (done) { 139 | var buf = Buffer.alloc(10240, '.') 140 | var server = createServer({ limit: '8kb' }) 141 | var test = request(server).post('/') 142 | test.set('Content-Type', 'application/octet-stream') 143 | test.write(buf) 144 | test.write(buf) 145 | test.write(buf) 146 | test.expect(413, done) 147 | }) 148 | 149 | it('should not error when inflating', function (done) { 150 | var server = createServer({ limit: '1kb' }) 151 | var test = request(server).post('/') 152 | test.set('Content-Encoding', 'gzip') 153 | test.set('Content-Type', 'application/octet-stream') 154 | test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a147040400', 'hex')) 155 | test.expect(413, done) 156 | }) 157 | }) 158 | 159 | describe('with inflate option', function () { 160 | describe('when false', function () { 161 | before(function () { 162 | this.server = createServer({ inflate: false }) 163 | }) 164 | 165 | it('should not accept content-encoding', function (done) { 166 | var test = request(this.server).post('/') 167 | test.set('Content-Encoding', 'gzip') 168 | test.set('Content-Type', 'application/octet-stream') 169 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) 170 | test.expect(415, '[encoding.unsupported] content encoding unsupported', done) 171 | }) 172 | }) 173 | 174 | describe('when true', function () { 175 | before(function () { 176 | this.server = createServer({ inflate: true }) 177 | }) 178 | 179 | it('should accept content-encoding', function (done) { 180 | var test = request(this.server).post('/') 181 | test.set('Content-Encoding', 'gzip') 182 | test.set('Content-Type', 'application/octet-stream') 183 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) 184 | test.expect(200, 'buf:6e616d653de8aeba', done) 185 | }) 186 | }) 187 | }) 188 | 189 | describe('with type option', function () { 190 | describe('when "application/vnd+octets"', function () { 191 | before(function () { 192 | this.server = createServer({ type: 'application/vnd+octets' }) 193 | }) 194 | 195 | it('should parse for custom type', function (done) { 196 | var test = request(this.server).post('/') 197 | test.set('Content-Type', 'application/vnd+octets') 198 | test.write(Buffer.from('000102', 'hex')) 199 | test.expect(200, 'buf:000102', done) 200 | }) 201 | 202 | it('should ignore standard type', function (done) { 203 | var test = request(this.server).post('/') 204 | test.set('Content-Type', 'application/octet-stream') 205 | test.write(Buffer.from('000102', 'hex')) 206 | test.expect(200, 'undefined', done) 207 | }) 208 | }) 209 | 210 | describe('when ["application/octet-stream", "application/vnd+octets"]', function () { 211 | before(function () { 212 | this.server = createServer({ 213 | type: ['application/octet-stream', 'application/vnd+octets'] 214 | }) 215 | }) 216 | 217 | it('should parse "application/octet-stream"', function (done) { 218 | var test = request(this.server).post('/') 219 | test.set('Content-Type', 'application/octet-stream') 220 | test.write(Buffer.from('000102', 'hex')) 221 | test.expect(200, 'buf:000102', done) 222 | }) 223 | 224 | it('should parse "application/vnd+octets"', function (done) { 225 | var test = request(this.server).post('/') 226 | test.set('Content-Type', 'application/vnd+octets') 227 | test.write(Buffer.from('000102', 'hex')) 228 | test.expect(200, 'buf:000102', done) 229 | }) 230 | 231 | it('should ignore "application/x-foo"', function (done) { 232 | var test = request(this.server).post('/') 233 | test.set('Content-Type', 'application/x-foo') 234 | test.write(Buffer.from('000102', 'hex')) 235 | test.expect(200, 'undefined', done) 236 | }) 237 | }) 238 | 239 | describe('when a function', function () { 240 | it('should parse when truthy value returned', function (done) { 241 | var server = createServer({ type: accept }) 242 | 243 | function accept (req) { 244 | return req.headers['content-type'] === 'application/vnd.octet' 245 | } 246 | 247 | var test = request(server).post('/') 248 | test.set('Content-Type', 'application/vnd.octet') 249 | test.write(Buffer.from('000102', 'hex')) 250 | test.expect(200, 'buf:000102', done) 251 | }) 252 | 253 | it('should work without content-type', function (done) { 254 | var server = createServer({ type: accept }) 255 | 256 | function accept (req) { 257 | return true 258 | } 259 | 260 | var test = request(server).post('/') 261 | test.write(Buffer.from('000102', 'hex')) 262 | test.expect(200, 'buf:000102', done) 263 | }) 264 | 265 | it('should not invoke without a body', function (done) { 266 | var server = createServer({ type: accept }) 267 | 268 | function accept (req) { 269 | throw new Error('oops!') 270 | } 271 | 272 | request(server) 273 | .get('/') 274 | .expect(200, done) 275 | }) 276 | }) 277 | }) 278 | 279 | describe('with verify option', function () { 280 | it('should assert value is function', function () { 281 | assert.throws(createServer.bind(null, { verify: 'lol' }), 282 | /TypeError: option verify must be function/) 283 | }) 284 | 285 | it('should error from verify', function (done) { 286 | var server = createServer({ 287 | verify: function (req, res, buf) { 288 | if (buf[0] === 0x00) throw new Error('no leading null') 289 | } 290 | }) 291 | 292 | var test = request(server).post('/') 293 | test.set('Content-Type', 'application/octet-stream') 294 | test.write(Buffer.from('000102', 'hex')) 295 | test.expect(403, '[entity.verify.failed] no leading null', done) 296 | }) 297 | 298 | it('should allow custom codes', function (done) { 299 | var server = createServer({ 300 | verify: function (req, res, buf) { 301 | if (buf[0] !== 0x00) return 302 | var err = new Error('no leading null') 303 | err.status = 400 304 | throw err 305 | } 306 | }) 307 | 308 | var test = request(server).post('/') 309 | test.set('Content-Type', 'application/octet-stream') 310 | test.write(Buffer.from('000102', 'hex')) 311 | test.expect(400, '[entity.verify.failed] no leading null', done) 312 | }) 313 | 314 | it('should allow pass-through', function (done) { 315 | var server = createServer({ 316 | verify: function (req, res, buf) { 317 | if (buf[0] === 0x00) throw new Error('no leading null') 318 | } 319 | }) 320 | 321 | var test = request(server).post('/') 322 | test.set('Content-Type', 'application/octet-stream') 323 | test.write(Buffer.from('0102', 'hex')) 324 | test.expect(200, 'buf:0102', done) 325 | }) 326 | }) 327 | 328 | describe('async local storage', function () { 329 | before(function () { 330 | var rawParser = bodyParser.raw() 331 | var store = { foo: 'bar' } 332 | 333 | this.server = createServer(function (req, res, next) { 334 | var asyncLocalStorage = new AsyncLocalStorage() 335 | 336 | asyncLocalStorage.run(store, function () { 337 | rawParser(req, res, function (err) { 338 | var local = asyncLocalStorage.getStore() 339 | 340 | if (local) { 341 | res.setHeader('x-store-foo', String(local.foo)) 342 | } 343 | 344 | next(err) 345 | }) 346 | }) 347 | }) 348 | }) 349 | 350 | it('should presist store', function (done) { 351 | request(this.server) 352 | .post('/') 353 | .set('Content-Type', 'application/octet-stream') 354 | .send('the user is tobi') 355 | .expect(200) 356 | .expect('x-store-foo', 'bar') 357 | .expect('buf:746865207573657220697320746f6269') 358 | .end(done) 359 | }) 360 | 361 | it('should presist store when unmatched content-type', function (done) { 362 | request(this.server) 363 | .post('/') 364 | .set('Content-Type', 'application/fizzbuzz') 365 | .send('buzz') 366 | .expect(200) 367 | .expect('x-store-foo', 'bar') 368 | .expect('undefined') 369 | .end(done) 370 | }) 371 | 372 | it('should presist store when inflated', function (done) { 373 | var test = request(this.server).post('/') 374 | test.set('Content-Encoding', 'gzip') 375 | test.set('Content-Type', 'application/octet-stream') 376 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) 377 | test.expect(200) 378 | test.expect('x-store-foo', 'bar') 379 | test.expect('buf:6e616d653de8aeba') 380 | test.end(done) 381 | }) 382 | 383 | it('should presist store when inflate error', function (done) { 384 | var test = request(this.server).post('/') 385 | test.set('Content-Encoding', 'gzip') 386 | test.set('Content-Type', 'application/octet-stream') 387 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex')) 388 | test.expect(400) 389 | test.expect('x-store-foo', 'bar') 390 | test.end(done) 391 | }) 392 | 393 | it('should presist store when limit exceeded', function (done) { 394 | request(this.server) 395 | .post('/') 396 | .set('Content-Type', 'application/octet-stream') 397 | .send('the user is ' + Buffer.alloc(1024 * 100, '.').toString()) 398 | .expect(413) 399 | .expect('x-store-foo', 'bar') 400 | .end(done) 401 | }) 402 | }) 403 | 404 | describe('charset', function () { 405 | before(function () { 406 | this.server = createServer() 407 | }) 408 | 409 | it('should ignore charset', function (done) { 410 | var test = request(this.server).post('/') 411 | test.set('Content-Type', 'application/octet-stream; charset=utf-8') 412 | test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) 413 | test.expect(200, 'buf:6e616d6520697320e8aeba', done) 414 | }) 415 | }) 416 | 417 | describe('encoding', function () { 418 | before(function () { 419 | this.server = createServer({ limit: '10kb' }) 420 | }) 421 | 422 | it('should parse without encoding', function (done) { 423 | var test = request(this.server).post('/') 424 | test.set('Content-Type', 'application/octet-stream') 425 | test.write(Buffer.from('6e616d653de8aeba', 'hex')) 426 | test.expect(200, 'buf:6e616d653de8aeba', done) 427 | }) 428 | 429 | it('should support identity encoding', function (done) { 430 | var test = request(this.server).post('/') 431 | test.set('Content-Encoding', 'identity') 432 | test.set('Content-Type', 'application/octet-stream') 433 | test.write(Buffer.from('6e616d653de8aeba', 'hex')) 434 | test.expect(200, 'buf:6e616d653de8aeba', done) 435 | }) 436 | 437 | it('should support gzip encoding', function (done) { 438 | var test = request(this.server).post('/') 439 | test.set('Content-Encoding', 'gzip') 440 | test.set('Content-Type', 'application/octet-stream') 441 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) 442 | test.expect(200, 'buf:6e616d653de8aeba', done) 443 | }) 444 | 445 | it('should support deflate encoding', function (done) { 446 | var test = request(this.server).post('/') 447 | test.set('Content-Encoding', 'deflate') 448 | test.set('Content-Type', 'application/octet-stream') 449 | test.write(Buffer.from('789ccb4bcc4db57db16e17001068042f', 'hex')) 450 | test.expect(200, 'buf:6e616d653de8aeba', done) 451 | }) 452 | 453 | it('should support brotli encoding', function (done) { 454 | var test = request(this.server).post('/') 455 | test.set('Content-Encoding', 'br') 456 | test.set('Content-Type', 'application/octet-stream') 457 | test.write(Buffer.from('8b03806e616d653de8aeba03', 'hex')) 458 | test.expect(200, 'buf:6e616d653de8aeba', done) 459 | }) 460 | 461 | it('should be case-insensitive', function (done) { 462 | var test = request(this.server).post('/') 463 | test.set('Content-Encoding', 'GZIP') 464 | test.set('Content-Type', 'application/octet-stream') 465 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) 466 | test.expect(200, 'buf:6e616d653de8aeba', done) 467 | }) 468 | 469 | it('should 415 on unknown encoding', function (done) { 470 | var test = request(this.server).post('/') 471 | test.set('Content-Encoding', 'nulls') 472 | test.set('Content-Type', 'application/octet-stream') 473 | test.write(Buffer.from('000000000000', 'hex')) 474 | test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) 475 | }) 476 | }) 477 | }) 478 | 479 | function createServer (opts) { 480 | var _bodyParser = typeof opts !== 'function' 481 | ? bodyParser.raw(opts) 482 | : opts 483 | 484 | return http.createServer(function (req, res) { 485 | _bodyParser(req, res, function (err) { 486 | if (err) { 487 | res.statusCode = err.status || 500 488 | res.end('[' + err.type + '] ' + err.message) 489 | return 490 | } 491 | 492 | if (Buffer.isBuffer(req.body)) { 493 | res.end('buf:' + req.body.toString('hex')) 494 | return 495 | } 496 | 497 | res.end(JSON.stringify(req.body) || typeof req.body) 498 | }) 499 | }) 500 | } 501 | -------------------------------------------------------------------------------- /test/text.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('node:assert') 4 | var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage 5 | var http = require('node:http') 6 | var request = require('supertest') 7 | 8 | var bodyParser = require('..') 9 | 10 | describe('bodyParser.text()', function () { 11 | before(function () { 12 | this.server = createServer() 13 | }) 14 | 15 | it('should parse text/plain', function (done) { 16 | request(this.server) 17 | .post('/') 18 | .set('Content-Type', 'text/plain') 19 | .send('user is tobi') 20 | .expect(200, '"user is tobi"', done) 21 | }) 22 | 23 | it('should 400 when invalid content-length', function (done) { 24 | var textParser = bodyParser.text() 25 | var server = createServer(function (req, res, next) { 26 | req.headers['content-length'] = '20' // bad length 27 | textParser(req, res, next) 28 | }) 29 | 30 | request(server) 31 | .post('/') 32 | .set('Content-Type', 'text/plain') 33 | .send('user') 34 | .expect(400, /content length/, done) 35 | }) 36 | 37 | it('should handle Content-Length: 0', function (done) { 38 | request(createServer({ limit: '1kb' })) 39 | .post('/') 40 | .set('Content-Type', 'text/plain') 41 | .set('Content-Length', '0') 42 | .expect(200, '""', done) 43 | }) 44 | 45 | it('should handle empty message-body', function (done) { 46 | request(createServer({ limit: '1kb' })) 47 | .post('/') 48 | .set('Content-Type', 'text/plain') 49 | .set('Transfer-Encoding', 'chunked') 50 | .send('') 51 | .expect(200, '""', done) 52 | }) 53 | 54 | it('should handle consumed stream', function (done) { 55 | var textParser = bodyParser.text() 56 | var server = createServer(function (req, res, next) { 57 | req.on('end', function () { 58 | textParser(req, res, next) 59 | }) 60 | req.resume() 61 | }) 62 | 63 | request(server) 64 | .post('/') 65 | .set('Content-Type', 'text/plain') 66 | .send('user is tobi') 67 | .expect(200, 'undefined', done) 68 | }) 69 | 70 | it('should handle duplicated middleware', function (done) { 71 | var textParser = bodyParser.text() 72 | var server = createServer(function (req, res, next) { 73 | textParser(req, res, function (err) { 74 | if (err) return next(err) 75 | textParser(req, res, next) 76 | }) 77 | }) 78 | 79 | request(server) 80 | .post('/') 81 | .set('Content-Type', 'text/plain') 82 | .send('user is tobi') 83 | .expect(200, '"user is tobi"', done) 84 | }) 85 | 86 | describe('with defaultCharset option', function () { 87 | it('should change default charset', function (done) { 88 | var server = createServer({ defaultCharset: 'koi8-r' }) 89 | var test = request(server).post('/') 90 | test.set('Content-Type', 'text/plain') 91 | test.write(Buffer.from('6e616d6520697320cec5d4', 'hex')) 92 | test.expect(200, '"name is нет"', done) 93 | }) 94 | 95 | it('should honor content-type charset', function (done) { 96 | var server = createServer({ defaultCharset: 'koi8-r' }) 97 | var test = request(server).post('/') 98 | test.set('Content-Type', 'text/plain; charset=utf-8') 99 | test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) 100 | test.expect(200, '"name is 论"', done) 101 | }) 102 | }) 103 | 104 | describe('with limit option', function () { 105 | it('should 413 when over limit with Content-Length', function (done) { 106 | var buf = Buffer.alloc(1028, '.') 107 | request(createServer({ limit: '1kb' })) 108 | .post('/') 109 | .set('Content-Type', 'text/plain') 110 | .set('Content-Length', '1028') 111 | .send(buf.toString()) 112 | .expect(413, done) 113 | }) 114 | 115 | it('should 413 when over limit with chunked encoding', function (done) { 116 | var buf = Buffer.alloc(1028, '.') 117 | var server = createServer({ limit: '1kb' }) 118 | var test = request(server).post('/') 119 | test.set('Content-Type', 'text/plain') 120 | test.set('Transfer-Encoding', 'chunked') 121 | test.write(buf.toString()) 122 | test.expect(413, done) 123 | }) 124 | 125 | it('should 413 when inflated body over limit', function (done) { 126 | var server = createServer({ limit: '1kb' }) 127 | var test = request(server).post('/') 128 | test.set('Content-Encoding', 'gzip') 129 | test.set('Content-Type', 'text/plain') 130 | test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a14704040000', 'hex')) 131 | test.expect(413, done) 132 | }) 133 | 134 | it('should accept number of bytes', function (done) { 135 | var buf = Buffer.alloc(1028, '.') 136 | request(createServer({ limit: 1024 })) 137 | .post('/') 138 | .set('Content-Type', 'text/plain') 139 | .send(buf.toString()) 140 | .expect(413, done) 141 | }) 142 | 143 | it('should not change when options altered', function (done) { 144 | var buf = Buffer.alloc(1028, '.') 145 | var options = { limit: '1kb' } 146 | var server = createServer(options) 147 | 148 | options.limit = '100kb' 149 | 150 | request(server) 151 | .post('/') 152 | .set('Content-Type', 'text/plain') 153 | .send(buf.toString()) 154 | .expect(413, done) 155 | }) 156 | 157 | it('should not hang response', function (done) { 158 | var buf = Buffer.alloc(10240, '.') 159 | var server = createServer({ limit: '8kb' }) 160 | var test = request(server).post('/') 161 | test.set('Content-Type', 'text/plain') 162 | test.write(buf) 163 | test.write(buf) 164 | test.write(buf) 165 | test.expect(413, done) 166 | }) 167 | 168 | it('should not error when inflating', function (done) { 169 | var server = createServer({ limit: '1kb' }) 170 | var test = request(server).post('/') 171 | test.set('Content-Encoding', 'gzip') 172 | test.set('Content-Type', 'text/plain') 173 | test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a1470404', 'hex')) 174 | setTimeout(function () { 175 | test.expect(413, done) 176 | }, 100) 177 | }) 178 | }) 179 | 180 | describe('with inflate option', function () { 181 | describe('when false', function () { 182 | before(function () { 183 | this.server = createServer({ inflate: false }) 184 | }) 185 | 186 | it('should not accept content-encoding', function (done) { 187 | var test = request(this.server).post('/') 188 | test.set('Content-Encoding', 'gzip') 189 | test.set('Content-Type', 'text/plain') 190 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) 191 | test.expect(415, '[encoding.unsupported] content encoding unsupported', done) 192 | }) 193 | }) 194 | 195 | describe('when true', function () { 196 | before(function () { 197 | this.server = createServer({ inflate: true }) 198 | }) 199 | 200 | it('should accept content-encoding', function (done) { 201 | var test = request(this.server).post('/') 202 | test.set('Content-Encoding', 'gzip') 203 | test.set('Content-Type', 'text/plain') 204 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) 205 | test.expect(200, '"name is 论"', done) 206 | }) 207 | }) 208 | }) 209 | 210 | describe('with type option', function () { 211 | describe('when "text/html"', function () { 212 | before(function () { 213 | this.server = createServer({ type: 'text/html' }) 214 | }) 215 | 216 | it('should parse for custom type', function (done) { 217 | request(this.server) 218 | .post('/') 219 | .set('Content-Type', 'text/html') 220 | .send('tobi') 221 | .expect(200, '"tobi"', done) 222 | }) 223 | 224 | it('should ignore standard type', function (done) { 225 | request(this.server) 226 | .post('/') 227 | .set('Content-Type', 'text/plain') 228 | .send('user is tobi') 229 | .expect(200, 'undefined', done) 230 | }) 231 | }) 232 | 233 | describe('when ["text/html", "text/plain"]', function () { 234 | before(function () { 235 | this.server = createServer({ type: ['text/html', 'text/plain'] }) 236 | }) 237 | 238 | it('should parse "text/html"', function (done) { 239 | request(this.server) 240 | .post('/') 241 | .set('Content-Type', 'text/html') 242 | .send('tobi') 243 | .expect(200, '"tobi"', done) 244 | }) 245 | 246 | it('should parse "text/plain"', function (done) { 247 | request(this.server) 248 | .post('/') 249 | .set('Content-Type', 'text/plain') 250 | .send('tobi') 251 | .expect(200, '"tobi"', done) 252 | }) 253 | 254 | it('should ignore "text/xml"', function (done) { 255 | request(this.server) 256 | .post('/') 257 | .set('Content-Type', 'text/xml') 258 | .send('tobi') 259 | .expect(200, 'undefined', done) 260 | }) 261 | }) 262 | 263 | describe('when a function', function () { 264 | it('should parse when truthy value returned', function (done) { 265 | var server = createServer({ type: accept }) 266 | 267 | function accept (req) { 268 | return req.headers['content-type'] === 'text/vnd.something' 269 | } 270 | 271 | request(server) 272 | .post('/') 273 | .set('Content-Type', 'text/vnd.something') 274 | .send('user is tobi') 275 | .expect(200, '"user is tobi"', done) 276 | }) 277 | 278 | it('should work without content-type', function (done) { 279 | var server = createServer({ type: accept }) 280 | 281 | function accept (req) { 282 | return true 283 | } 284 | 285 | var test = request(server).post('/') 286 | test.write('user is tobi') 287 | test.expect(200, '"user is tobi"', done) 288 | }) 289 | 290 | it('should not invoke without a body', function (done) { 291 | var server = createServer({ type: accept }) 292 | 293 | function accept (req) { 294 | throw new Error('oops!') 295 | } 296 | 297 | request(server) 298 | .get('/') 299 | .expect(200, done) 300 | }) 301 | }) 302 | }) 303 | 304 | describe('with verify option', function () { 305 | it('should assert value is function', function () { 306 | assert.throws(createServer.bind(null, { verify: 'lol' }), 307 | /TypeError: option verify must be function/) 308 | }) 309 | 310 | it('should error from verify', function (done) { 311 | var server = createServer({ 312 | verify: function (req, res, buf) { 313 | if (buf[0] === 0x20) throw new Error('no leading space') 314 | } 315 | }) 316 | 317 | request(server) 318 | .post('/') 319 | .set('Content-Type', 'text/plain') 320 | .send(' user is tobi') 321 | .expect(403, '[entity.verify.failed] no leading space', done) 322 | }) 323 | 324 | it('should allow custom codes', function (done) { 325 | var server = createServer({ 326 | verify: function (req, res, buf) { 327 | if (buf[0] !== 0x20) return 328 | var err = new Error('no leading space') 329 | err.status = 400 330 | throw err 331 | } 332 | }) 333 | 334 | request(server) 335 | .post('/') 336 | .set('Content-Type', 'text/plain') 337 | .send(' user is tobi') 338 | .expect(400, '[entity.verify.failed] no leading space', done) 339 | }) 340 | 341 | it('should allow pass-through', function (done) { 342 | var server = createServer({ 343 | verify: function (req, res, buf) { 344 | if (buf[0] === 0x20) throw new Error('no leading space') 345 | } 346 | }) 347 | 348 | request(server) 349 | .post('/') 350 | .set('Content-Type', 'text/plain') 351 | .send('user is tobi') 352 | .expect(200, '"user is tobi"', done) 353 | }) 354 | 355 | it('should 415 on unknown charset prior to verify', function (done) { 356 | var server = createServer({ 357 | verify: function (req, res, buf) { 358 | throw new Error('unexpected verify call') 359 | } 360 | }) 361 | 362 | var test = request(server).post('/') 363 | test.set('Content-Type', 'text/plain; charset=x-bogus') 364 | test.write(Buffer.from('00000000', 'hex')) 365 | test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) 366 | }) 367 | }) 368 | 369 | describe('async local storage', function () { 370 | before(function () { 371 | var textParser = bodyParser.text() 372 | var store = { foo: 'bar' } 373 | 374 | this.server = createServer(function (req, res, next) { 375 | var asyncLocalStorage = new AsyncLocalStorage() 376 | 377 | asyncLocalStorage.run(store, function () { 378 | textParser(req, res, function (err) { 379 | var local = asyncLocalStorage.getStore() 380 | 381 | if (local) { 382 | res.setHeader('x-store-foo', String(local.foo)) 383 | } 384 | 385 | next(err) 386 | }) 387 | }) 388 | }) 389 | }) 390 | 391 | it('should presist store', function (done) { 392 | request(this.server) 393 | .post('/') 394 | .set('Content-Type', 'text/plain') 395 | .send('user is tobi') 396 | .expect(200) 397 | .expect('x-store-foo', 'bar') 398 | .expect('"user is tobi"') 399 | .end(done) 400 | }) 401 | 402 | it('should presist store when unmatched content-type', function (done) { 403 | request(this.server) 404 | .post('/') 405 | .set('Content-Type', 'application/fizzbuzz') 406 | .send('buzz') 407 | .expect(200) 408 | .expect('x-store-foo', 'bar') 409 | .expect('undefined') 410 | .end(done) 411 | }) 412 | 413 | it('should presist store when inflated', function (done) { 414 | var test = request(this.server).post('/') 415 | test.set('Content-Encoding', 'gzip') 416 | test.set('Content-Type', 'text/plain') 417 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) 418 | test.expect(200) 419 | test.expect('x-store-foo', 'bar') 420 | test.expect('"name is 论"') 421 | test.end(done) 422 | }) 423 | 424 | it('should presist store when inflate error', function (done) { 425 | var test = request(this.server).post('/') 426 | test.set('Content-Encoding', 'gzip') 427 | test.set('Content-Type', 'text/plain') 428 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b0000', 'hex')) 429 | test.expect(400) 430 | test.expect('x-store-foo', 'bar') 431 | test.end(done) 432 | }) 433 | 434 | it('should presist store when limit exceeded', function (done) { 435 | request(this.server) 436 | .post('/') 437 | .set('Content-Type', 'text/plain') 438 | .send('user is ' + Buffer.alloc(1024 * 100, '.').toString()) 439 | .expect(413) 440 | .expect('x-store-foo', 'bar') 441 | .end(done) 442 | }) 443 | }) 444 | 445 | describe('charset', function () { 446 | before(function () { 447 | this.server = createServer() 448 | }) 449 | 450 | it('should parse utf-8', function (done) { 451 | var test = request(this.server).post('/') 452 | test.set('Content-Type', 'text/plain; charset=utf-8') 453 | test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) 454 | test.expect(200, '"name is 论"', done) 455 | }) 456 | 457 | it('should parse codepage charsets', function (done) { 458 | var test = request(this.server).post('/') 459 | test.set('Content-Type', 'text/plain; charset=koi8-r') 460 | test.write(Buffer.from('6e616d6520697320cec5d4', 'hex')) 461 | test.expect(200, '"name is нет"', done) 462 | }) 463 | 464 | it('should parse when content-length != char length', function (done) { 465 | var test = request(this.server).post('/') 466 | test.set('Content-Type', 'text/plain; charset=utf-8') 467 | test.set('Content-Length', '11') 468 | test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) 469 | test.expect(200, '"name is 论"', done) 470 | }) 471 | 472 | it('should default to utf-8', function (done) { 473 | var test = request(this.server).post('/') 474 | test.set('Content-Type', 'text/plain') 475 | test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) 476 | test.expect(200, '"name is 论"', done) 477 | }) 478 | 479 | it('should 415 on unknown charset', function (done) { 480 | var test = request(this.server).post('/') 481 | test.set('Content-Type', 'text/plain; charset=x-bogus') 482 | test.write(Buffer.from('00000000', 'hex')) 483 | test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) 484 | }) 485 | }) 486 | 487 | describe('encoding', function () { 488 | before(function () { 489 | this.server = createServer({ limit: '10kb' }) 490 | }) 491 | 492 | it('should parse without encoding', function (done) { 493 | var test = request(this.server).post('/') 494 | test.set('Content-Type', 'text/plain') 495 | test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) 496 | test.expect(200, '"name is 论"', done) 497 | }) 498 | 499 | it('should support identity encoding', function (done) { 500 | var test = request(this.server).post('/') 501 | test.set('Content-Encoding', 'identity') 502 | test.set('Content-Type', 'text/plain') 503 | test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) 504 | test.expect(200, '"name is 论"', done) 505 | }) 506 | 507 | it('should support gzip encoding', function (done) { 508 | var test = request(this.server).post('/') 509 | test.set('Content-Encoding', 'gzip') 510 | test.set('Content-Type', 'text/plain') 511 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) 512 | test.expect(200, '"name is 论"', done) 513 | }) 514 | 515 | it('should support deflate encoding', function (done) { 516 | var test = request(this.server).post('/') 517 | test.set('Content-Encoding', 'deflate') 518 | test.set('Content-Type', 'text/plain') 519 | test.write(Buffer.from('789ccb4bcc4d55c82c5678b16e17001a6f050e', 'hex')) 520 | test.expect(200, '"name is 论"', done) 521 | }) 522 | 523 | it('should support brotli encoding', function (done) { 524 | var test = request(this.server).post('/') 525 | test.set('Content-Encoding', 'br') 526 | test.set('Content-Type', 'text/plain') 527 | test.write(Buffer.from('0b05806e616d6520697320e8aeba03', 'hex')) 528 | test.expect(200, '"name is 论"', done) 529 | }) 530 | 531 | it('should be case-insensitive', function (done) { 532 | var test = request(this.server).post('/') 533 | test.set('Content-Encoding', 'GZIP') 534 | test.set('Content-Type', 'text/plain') 535 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) 536 | test.expect(200, '"name is 论"', done) 537 | }) 538 | 539 | it('should 415 on unknown encoding', function (done) { 540 | var test = request(this.server).post('/') 541 | test.set('Content-Encoding', 'nulls') 542 | test.set('Content-Type', 'text/plain') 543 | test.write(Buffer.from('000000000000', 'hex')) 544 | test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) 545 | }) 546 | }) 547 | }) 548 | 549 | function createServer (opts) { 550 | var _bodyParser = typeof opts !== 'function' 551 | ? bodyParser.text(opts) 552 | : opts 553 | 554 | return http.createServer(function (req, res) { 555 | _bodyParser(req, res, function (err) { 556 | res.statusCode = err ? (err.status || 500) : 200 557 | res.end(err 558 | ? ('[' + err.type + '] ' + err.message) 559 | : (JSON.stringify(req.body) || typeof req.body) 560 | ) 561 | }) 562 | }) 563 | } 564 | -------------------------------------------------------------------------------- /test/urlencoded.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('node:assert') 4 | var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage 5 | var http = require('node:http') 6 | var request = require('supertest') 7 | 8 | var bodyParser = require('..') 9 | 10 | describe('bodyParser.urlencoded()', function () { 11 | before(function () { 12 | this.server = createServer() 13 | }) 14 | 15 | it('should parse x-www-form-urlencoded', function (done) { 16 | request(this.server) 17 | .post('/') 18 | .set('Content-Type', 'application/x-www-form-urlencoded') 19 | .send('user=tobi') 20 | .expect(200, '{"user":"tobi"}', done) 21 | }) 22 | 23 | it('should 400 when invalid content-length', function (done) { 24 | var urlencodedParser = bodyParser.urlencoded() 25 | var server = createServer(function (req, res, next) { 26 | req.headers['content-length'] = '20' // bad length 27 | urlencodedParser(req, res, next) 28 | }) 29 | 30 | request(server) 31 | .post('/') 32 | .set('Content-Type', 'application/x-www-form-urlencoded') 33 | .send('str=') 34 | .expect(400, /content length/, done) 35 | }) 36 | 37 | it('should handle Content-Length: 0', function (done) { 38 | request(this.server) 39 | .post('/') 40 | .set('Content-Type', 'application/x-www-form-urlencoded') 41 | .set('Content-Length', '0') 42 | .send('') 43 | .expect(200, '{}', done) 44 | }) 45 | 46 | var extendedValues = [true, false] 47 | extendedValues.forEach(function (extended) { 48 | describe('in ' + (extended ? 'extended' : 'simple') + ' mode', function () { 49 | it('should parse x-www-form-urlencoded with an explicit iso-8859-1 encoding', function (done) { 50 | var server = createServer({ extended: extended }) 51 | request(server) 52 | .post('/') 53 | .set('Content-Type', 'application/x-www-form-urlencoded; charset=iso-8859-1') 54 | .send('%A2=%BD') 55 | .expect(200, '{"¢":"½"}', done) 56 | }) 57 | 58 | it('should parse x-www-form-urlencoded with unspecified iso-8859-1 encoding when the defaultCharset is set to iso-8859-1', function (done) { 59 | var server = createServer({ defaultCharset: 'iso-8859-1', extended: extended }) 60 | request(server) 61 | .post('/') 62 | .set('Content-Type', 'application/x-www-form-urlencoded') 63 | .send('%A2=%BD') 64 | .expect(200, '{"¢":"½"}', done) 65 | }) 66 | 67 | it('should parse x-www-form-urlencoded with an unspecified iso-8859-1 encoding when the utf8 sentinel has a value of %26%2310003%3B', function (done) { 68 | var server = createServer({ charsetSentinel: true, extended: extended }) 69 | request(server) 70 | .post('/') 71 | .set('Content-Type', 'application/x-www-form-urlencoded') 72 | .send('utf8=%26%2310003%3B&user=%C3%B8') 73 | .expect(200, '{"user":"ø"}', done) 74 | }) 75 | 76 | it('should parse x-www-form-urlencoded with an unspecified utf-8 encoding when the utf8 sentinel has a value of %E2%9C%93 and the defaultCharset is iso-8859-1', function (done) { 77 | var server = createServer({ charsetSentinel: true, extended: extended }) 78 | request(server) 79 | .post('/') 80 | .set('Content-Type', 'application/x-www-form-urlencoded') 81 | .send('utf8=%E2%9C%93&user=%C3%B8') 82 | .expect(200, '{"user":"ø"}', done) 83 | }) 84 | 85 | it('should not leave an empty string parameter when removing the utf8 sentinel from the start of the string', function (done) { 86 | var server = createServer({ charsetSentinel: true, extended: extended }) 87 | request(server) 88 | .post('/') 89 | .set('Content-Type', 'application/x-www-form-urlencoded') 90 | .send('utf8=%E2%9C%93&foo=bar') 91 | .expect(200, '{"foo":"bar"}', done) 92 | }) 93 | 94 | it('should not leave an empty string parameter when removing the utf8 sentinel from the middle of the string', function (done) { 95 | var server = createServer({ charsetSentinel: true, extended: extended }) 96 | request(server) 97 | .post('/') 98 | .set('Content-Type', 'application/x-www-form-urlencoded') 99 | .send('foo=bar&utf8=%E2%9C%93&baz=quux') 100 | .expect(200, '{"foo":"bar","baz":"quux"}', done) 101 | }) 102 | 103 | it('should not leave an empty string parameter when removing the utf8 sentinel from the end of the string', function (done) { 104 | var server = createServer({ charsetSentinel: true, extended: extended }) 105 | request(server) 106 | .post('/') 107 | .set('Content-Type', 'application/x-www-form-urlencoded') 108 | .send('foo=bar&baz=quux&utf8=%E2%9C%93') 109 | .expect(200, '{"foo":"bar","baz":"quux"}', done) 110 | }) 111 | }) 112 | }) 113 | 114 | it('should handle empty message-body', function (done) { 115 | request(createServer({ limit: '1kb' })) 116 | .post('/') 117 | .set('Content-Type', 'application/x-www-form-urlencoded') 118 | .set('Transfer-Encoding', 'chunked') 119 | .send('') 120 | .expect(200, '{}', done) 121 | }) 122 | 123 | it('should handle consumed stream', function (done) { 124 | var urlencodedParser = bodyParser.urlencoded() 125 | var server = createServer(function (req, res, next) { 126 | req.on('end', function () { 127 | urlencodedParser(req, res, next) 128 | }) 129 | req.resume() 130 | }) 131 | 132 | request(server) 133 | .post('/') 134 | .set('Content-Type', 'application/x-www-form-urlencoded') 135 | .send('user=tobi') 136 | .expect(200, 'undefined', done) 137 | }) 138 | 139 | it('should handle duplicated middleware', function (done) { 140 | var urlencodedParser = bodyParser.urlencoded() 141 | var server = createServer(function (req, res, next) { 142 | urlencodedParser(req, res, function (err) { 143 | if (err) return next(err) 144 | urlencodedParser(req, res, next) 145 | }) 146 | }) 147 | 148 | request(server) 149 | .post('/') 150 | .set('Content-Type', 'application/x-www-form-urlencoded') 151 | .send('user=tobi') 152 | .expect(200, '{"user":"tobi"}', done) 153 | }) 154 | 155 | it('should not parse extended syntax', function (done) { 156 | request(this.server) 157 | .post('/') 158 | .set('Content-Type', 'application/x-www-form-urlencoded') 159 | .send('user[name][first]=Tobi') 160 | .expect(200, '{"user[name][first]":"Tobi"}', done) 161 | }) 162 | 163 | describe('with extended option', function () { 164 | describe('when false', function () { 165 | before(function () { 166 | this.server = createServer({ extended: false }) 167 | }) 168 | 169 | it('should not parse extended syntax', function (done) { 170 | request(this.server) 171 | .post('/') 172 | .set('Content-Type', 'application/x-www-form-urlencoded') 173 | .send('user[name][first]=Tobi') 174 | .expect(200, '{"user[name][first]":"Tobi"}', done) 175 | }) 176 | 177 | it('should parse multiple key instances', function (done) { 178 | request(this.server) 179 | .post('/') 180 | .set('Content-Type', 'application/x-www-form-urlencoded') 181 | .send('user=Tobi&user=Loki') 182 | .expect(200, '{"user":["Tobi","Loki"]}', done) 183 | }) 184 | }) 185 | 186 | describe('when true', function () { 187 | before(function () { 188 | this.server = createServer({ extended: true }) 189 | }) 190 | 191 | it('should parse multiple key instances', function (done) { 192 | request(this.server) 193 | .post('/') 194 | .set('Content-Type', 'application/x-www-form-urlencoded') 195 | .send('user=Tobi&user=Loki') 196 | .expect(200, '{"user":["Tobi","Loki"]}', done) 197 | }) 198 | 199 | it('should parse extended syntax', function (done) { 200 | request(this.server) 201 | .post('/') 202 | .set('Content-Type', 'application/x-www-form-urlencoded') 203 | .send('user[name][first]=Tobi') 204 | .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) 205 | }) 206 | 207 | it('should parse parameters with dots', function (done) { 208 | request(this.server) 209 | .post('/') 210 | .set('Content-Type', 'application/x-www-form-urlencoded') 211 | .send('user.name=Tobi') 212 | .expect(200, '{"user.name":"Tobi"}', done) 213 | }) 214 | 215 | it('should parse fully-encoded extended syntax', function (done) { 216 | request(this.server) 217 | .post('/') 218 | .set('Content-Type', 'application/x-www-form-urlencoded') 219 | .send('user%5Bname%5D%5Bfirst%5D=Tobi') 220 | .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) 221 | }) 222 | 223 | it('should parse array index notation', function (done) { 224 | request(this.server) 225 | .post('/') 226 | .set('Content-Type', 'application/x-www-form-urlencoded') 227 | .send('foo[0]=bar&foo[1]=baz') 228 | .expect(200, '{"foo":["bar","baz"]}', done) 229 | }) 230 | 231 | it('should parse array index notation with large array', function (done) { 232 | var str = 'f[0]=0' 233 | 234 | for (var i = 1; i < 500; i++) { 235 | str += '&f[' + i + ']=' + i.toString(16) 236 | } 237 | 238 | request(this.server) 239 | .post('/') 240 | .set('Content-Type', 'application/x-www-form-urlencoded') 241 | .send(str) 242 | .expect(function (res) { 243 | var obj = JSON.parse(res.text) 244 | assert.strictEqual(Object.keys(obj).length, 1) 245 | assert.strictEqual(Array.isArray(obj.f), true) 246 | assert.strictEqual(obj.f.length, 500) 247 | }) 248 | .expect(200, done) 249 | }) 250 | 251 | it('should parse array of objects syntax', function (done) { 252 | request(this.server) 253 | .post('/') 254 | .set('Content-Type', 'application/x-www-form-urlencoded') 255 | .send('foo[0][bar]=baz&foo[0][fizz]=buzz&foo[]=done!') 256 | .expect(200, '{"foo":[{"bar":"baz","fizz":"buzz"},"done!"]}', done) 257 | }) 258 | 259 | it('should parse deep object', function (done) { 260 | var str = 'foo' 261 | 262 | for (var i = 0; i < 32; i++) { 263 | str += '[p]' 264 | } 265 | 266 | str += '=bar' 267 | 268 | request(this.server) 269 | .post('/') 270 | .set('Content-Type', 'application/x-www-form-urlencoded') 271 | .send(str) 272 | .expect(function (res) { 273 | var obj = JSON.parse(res.text) 274 | assert.strictEqual(Object.keys(obj).length, 1) 275 | assert.strictEqual(typeof obj.foo, 'object') 276 | 277 | var depth = 0 278 | var ref = obj.foo 279 | while ((ref = ref.p)) { depth++ } 280 | assert.strictEqual(depth, 32) 281 | }) 282 | .expect(200, done) 283 | }) 284 | }) 285 | }) 286 | 287 | describe('with depth option', function () { 288 | describe('when custom value set', function () { 289 | it('should reject non positive numbers', function () { 290 | assert.throws(createServer.bind(null, { extended: true, depth: -1 }), 291 | /TypeError: option depth must be a zero or a positive number/) 292 | assert.throws(createServer.bind(null, { extended: true, depth: NaN }), 293 | /TypeError: option depth must be a zero or a positive number/) 294 | assert.throws(createServer.bind(null, { extended: true, depth: 'beep' }), 295 | /TypeError: option depth must be a zero or a positive number/) 296 | }) 297 | 298 | it('should parse up to the specified depth', function (done) { 299 | this.server = createServer({ extended: true, depth: 10 }) 300 | request(this.server) 301 | .post('/') 302 | .set('Content-Type', 'application/x-www-form-urlencoded') 303 | .send('a[b][c][d]=value') 304 | .expect(200, '{"a":{"b":{"c":{"d":"value"}}}}', done) 305 | }) 306 | 307 | it('should not parse beyond the specified depth', function (done) { 308 | this.server = createServer({ extended: true, depth: 1 }) 309 | request(this.server) 310 | .post('/') 311 | .set('Content-Type', 'application/x-www-form-urlencoded') 312 | .send('a[b][c][d][e]=value') 313 | .expect(400, '[querystring.parse.rangeError] The input exceeded the depth', done) 314 | }) 315 | }) 316 | 317 | describe('when default value', function () { 318 | before(function () { 319 | this.server = createServer({ extended: true }) 320 | }) 321 | 322 | it('should parse deeply nested objects', function (done) { 323 | var deepObject = 'a' 324 | for (var i = 0; i < 32; i++) { 325 | deepObject += '[p]' 326 | } 327 | deepObject += '=value' 328 | 329 | request(this.server) 330 | .post('/') 331 | .set('Content-Type', 'application/x-www-form-urlencoded') 332 | .send(deepObject) 333 | .expect(function (res) { 334 | var obj = JSON.parse(res.text) 335 | var depth = 0 336 | var ref = obj.a 337 | while ((ref = ref.p)) { depth++ } 338 | assert.strictEqual(depth, 32) 339 | }) 340 | .expect(200, done) 341 | }) 342 | 343 | it('should not parse beyond the specified depth', function (done) { 344 | var deepObject = 'a' 345 | for (var i = 0; i < 33; i++) { 346 | deepObject += '[p]' 347 | } 348 | deepObject += '=value' 349 | 350 | request(this.server) 351 | .post('/') 352 | .set('Content-Type', 'application/x-www-form-urlencoded') 353 | .send(deepObject) 354 | .expect(400, '[querystring.parse.rangeError] The input exceeded the depth', done) 355 | }) 356 | }) 357 | }) 358 | 359 | describe('with inflate option', function () { 360 | describe('when false', function () { 361 | before(function () { 362 | this.server = createServer({ inflate: false }) 363 | }) 364 | 365 | it('should not accept content-encoding', function (done) { 366 | var test = request(this.server).post('/') 367 | test.set('Content-Encoding', 'gzip') 368 | test.set('Content-Type', 'application/x-www-form-urlencoded') 369 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) 370 | test.expect(415, '[encoding.unsupported] content encoding unsupported', done) 371 | }) 372 | }) 373 | 374 | describe('when true', function () { 375 | before(function () { 376 | this.server = createServer({ inflate: true }) 377 | }) 378 | 379 | it('should accept content-encoding', function (done) { 380 | var test = request(this.server).post('/') 381 | test.set('Content-Encoding', 'gzip') 382 | test.set('Content-Type', 'application/x-www-form-urlencoded') 383 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) 384 | test.expect(200, '{"name":"论"}', done) 385 | }) 386 | }) 387 | }) 388 | 389 | describe('with limit option', function () { 390 | it('should 413 when over limit with Content-Length', function (done) { 391 | var buf = Buffer.alloc(1024, '.') 392 | request(createServer({ limit: '1kb' })) 393 | .post('/') 394 | .set('Content-Type', 'application/x-www-form-urlencoded') 395 | .set('Content-Length', '1028') 396 | .send('str=' + buf.toString()) 397 | .expect(413, done) 398 | }) 399 | 400 | it('should 413 when over limit with chunked encoding', function (done) { 401 | var buf = Buffer.alloc(1024, '.') 402 | var server = createServer({ limit: '1kb' }) 403 | var test = request(server).post('/') 404 | test.set('Content-Type', 'application/x-www-form-urlencoded') 405 | test.set('Transfer-Encoding', 'chunked') 406 | test.write('str=') 407 | test.write(buf.toString()) 408 | test.expect(413, done) 409 | }) 410 | 411 | it('should 413 when inflated body over limit', function (done) { 412 | var server = createServer({ limit: '1kb' }) 413 | var test = request(server).post('/') 414 | test.set('Content-Encoding', 'gzip') 415 | test.set('Content-Type', 'application/x-www-form-urlencoded') 416 | test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f9204040000', 'hex')) 417 | test.expect(413, done) 418 | }) 419 | 420 | it('should accept number of bytes', function (done) { 421 | var buf = Buffer.alloc(1024, '.') 422 | request(createServer({ limit: 1024 })) 423 | .post('/') 424 | .set('Content-Type', 'application/x-www-form-urlencoded') 425 | .send('str=' + buf.toString()) 426 | .expect(413, done) 427 | }) 428 | 429 | it('should not change when options altered', function (done) { 430 | var buf = Buffer.alloc(1024, '.') 431 | var options = { limit: '1kb' } 432 | var server = createServer(options) 433 | 434 | options.limit = '100kb' 435 | 436 | request(server) 437 | .post('/') 438 | .set('Content-Type', 'application/x-www-form-urlencoded') 439 | .send('str=' + buf.toString()) 440 | .expect(413, done) 441 | }) 442 | 443 | it('should not hang response', function (done) { 444 | var buf = Buffer.alloc(10240, '.') 445 | var server = createServer({ limit: '8kb' }) 446 | var test = request(server).post('/') 447 | test.set('Content-Type', 'application/x-www-form-urlencoded') 448 | test.write(buf) 449 | test.write(buf) 450 | test.write(buf) 451 | test.expect(413, done) 452 | }) 453 | 454 | it('should not error when inflating', function (done) { 455 | var server = createServer({ limit: '1kb' }) 456 | var test = request(server).post('/') 457 | test.set('Content-Encoding', 'gzip') 458 | test.set('Content-Type', 'application/x-www-form-urlencoded') 459 | test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f92040400', 'hex')) 460 | test.expect(413, done) 461 | }) 462 | }) 463 | 464 | describe('with parameterLimit option', function () { 465 | describe('with extended: false', function () { 466 | it('should reject 0', function () { 467 | assert.throws(createServer.bind(null, { extended: false, parameterLimit: 0 }), 468 | /TypeError: option parameterLimit must be a positive number/) 469 | }) 470 | 471 | it('should reject string', function () { 472 | assert.throws(createServer.bind(null, { extended: false, parameterLimit: 'beep' }), 473 | /TypeError: option parameterLimit must be a positive number/) 474 | }) 475 | 476 | it('should 413 if over limit', function (done) { 477 | request(createServer({ extended: false, parameterLimit: 10 })) 478 | .post('/') 479 | .set('Content-Type', 'application/x-www-form-urlencoded') 480 | .send(createManyParams(11)) 481 | .expect(413, '[parameters.too.many] too many parameters', done) 482 | }) 483 | 484 | it('should work when at the limit', function (done) { 485 | request(createServer({ extended: false, parameterLimit: 10 })) 486 | .post('/') 487 | .set('Content-Type', 'application/x-www-form-urlencoded') 488 | .send(createManyParams(10)) 489 | .expect(expectKeyCount(10)) 490 | .expect(200, done) 491 | }) 492 | 493 | it('should work if number is floating point', function (done) { 494 | request(createServer({ extended: false, parameterLimit: 10.1 })) 495 | .post('/') 496 | .set('Content-Type', 'application/x-www-form-urlencoded') 497 | .send(createManyParams(11)) 498 | .expect(413, /too many parameters/, done) 499 | }) 500 | 501 | it('should work with large limit', function (done) { 502 | request(createServer({ extended: false, parameterLimit: 5000 })) 503 | .post('/') 504 | .set('Content-Type', 'application/x-www-form-urlencoded') 505 | .send(createManyParams(5000)) 506 | .expect(expectKeyCount(5000)) 507 | .expect(200, done) 508 | }) 509 | 510 | it('should work with Infinity limit', function (done) { 511 | request(createServer({ extended: false, parameterLimit: Infinity })) 512 | .post('/') 513 | .set('Content-Type', 'application/x-www-form-urlencoded') 514 | .send(createManyParams(10000)) 515 | .expect(expectKeyCount(10000)) 516 | .expect(200, done) 517 | }) 518 | }) 519 | 520 | describe('with extended: true', function () { 521 | it('should reject 0', function () { 522 | assert.throws(createServer.bind(null, { extended: true, parameterLimit: 0 }), 523 | /TypeError: option parameterLimit must be a positive number/) 524 | }) 525 | 526 | it('should reject string', function () { 527 | assert.throws(createServer.bind(null, { extended: true, parameterLimit: 'beep' }), 528 | /TypeError: option parameterLimit must be a positive number/) 529 | }) 530 | 531 | it('should 413 if over limit', function (done) { 532 | request(createServer({ extended: true, parameterLimit: 10 })) 533 | .post('/') 534 | .set('Content-Type', 'application/x-www-form-urlencoded') 535 | .send(createManyParams(11)) 536 | .expect(413, '[parameters.too.many] too many parameters', done) 537 | }) 538 | 539 | it('should work when at the limit', function (done) { 540 | request(createServer({ extended: true, parameterLimit: 10 })) 541 | .post('/') 542 | .set('Content-Type', 'application/x-www-form-urlencoded') 543 | .send(createManyParams(10)) 544 | .expect(expectKeyCount(10)) 545 | .expect(200, done) 546 | }) 547 | 548 | it('should work if number is floating point', function (done) { 549 | request(createServer({ extended: true, parameterLimit: 10.1 })) 550 | .post('/') 551 | .set('Content-Type', 'application/x-www-form-urlencoded') 552 | .send(createManyParams(11)) 553 | .expect(413, /too many parameters/, done) 554 | }) 555 | 556 | it('should work with large limit', function (done) { 557 | request(createServer({ extended: true, parameterLimit: 5000 })) 558 | .post('/') 559 | .set('Content-Type', 'application/x-www-form-urlencoded') 560 | .send(createManyParams(5000)) 561 | .expect(expectKeyCount(5000)) 562 | .expect(200, done) 563 | }) 564 | 565 | it('should work with Infinity limit', function (done) { 566 | request(createServer({ extended: true, parameterLimit: Infinity })) 567 | .post('/') 568 | .set('Content-Type', 'application/x-www-form-urlencoded') 569 | .send(createManyParams(10000)) 570 | .expect(expectKeyCount(10000)) 571 | .expect(200, done) 572 | }) 573 | }) 574 | }) 575 | 576 | describe('with type option', function () { 577 | describe('when "application/vnd.x-www-form-urlencoded"', function () { 578 | before(function () { 579 | this.server = createServer({ type: 'application/vnd.x-www-form-urlencoded' }) 580 | }) 581 | 582 | it('should parse for custom type', function (done) { 583 | request(this.server) 584 | .post('/') 585 | .set('Content-Type', 'application/vnd.x-www-form-urlencoded') 586 | .send('user=tobi') 587 | .expect(200, '{"user":"tobi"}', done) 588 | }) 589 | 590 | it('should ignore standard type', function (done) { 591 | request(this.server) 592 | .post('/') 593 | .set('Content-Type', 'application/x-www-form-urlencoded') 594 | .send('user=tobi') 595 | .expect(200, 'undefined', done) 596 | }) 597 | }) 598 | 599 | describe('when ["urlencoded", "application/x-pairs"]', function () { 600 | before(function () { 601 | this.server = createServer({ 602 | type: ['urlencoded', 'application/x-pairs'] 603 | }) 604 | }) 605 | 606 | it('should parse "application/x-www-form-urlencoded"', function (done) { 607 | request(this.server) 608 | .post('/') 609 | .set('Content-Type', 'application/x-www-form-urlencoded') 610 | .send('user=tobi') 611 | .expect(200, '{"user":"tobi"}', done) 612 | }) 613 | 614 | it('should parse "application/x-pairs"', function (done) { 615 | request(this.server) 616 | .post('/') 617 | .set('Content-Type', 'application/x-pairs') 618 | .send('user=tobi') 619 | .expect(200, '{"user":"tobi"}', done) 620 | }) 621 | 622 | it('should ignore application/x-foo', function (done) { 623 | request(this.server) 624 | .post('/') 625 | .set('Content-Type', 'application/x-foo') 626 | .send('user=tobi') 627 | .expect(200, 'undefined', done) 628 | }) 629 | }) 630 | 631 | describe('when a function', function () { 632 | it('should parse when truthy value returned', function (done) { 633 | var server = createServer({ type: accept }) 634 | 635 | function accept (req) { 636 | return req.headers['content-type'] === 'application/vnd.something' 637 | } 638 | 639 | request(server) 640 | .post('/') 641 | .set('Content-Type', 'application/vnd.something') 642 | .send('user=tobi') 643 | .expect(200, '{"user":"tobi"}', done) 644 | }) 645 | 646 | it('should work without content-type', function (done) { 647 | var server = createServer({ type: accept }) 648 | 649 | function accept (req) { 650 | return true 651 | } 652 | 653 | var test = request(server).post('/') 654 | test.write('user=tobi') 655 | test.expect(200, '{"user":"tobi"}', done) 656 | }) 657 | 658 | it('should not invoke without a body', function (done) { 659 | var server = createServer({ type: accept }) 660 | 661 | function accept (req) { 662 | throw new Error('oops!') 663 | } 664 | 665 | request(server) 666 | .get('/') 667 | .expect(200, done) 668 | }) 669 | }) 670 | }) 671 | 672 | describe('with verify option', function () { 673 | it('should assert value if function', function () { 674 | assert.throws(createServer.bind(null, { verify: 'lol' }), 675 | /TypeError: option verify must be function/) 676 | }) 677 | 678 | it('should error from verify', function (done) { 679 | var server = createServer({ 680 | verify: function (req, res, buf) { 681 | if (buf[0] === 0x20) throw new Error('no leading space') 682 | } 683 | }) 684 | 685 | request(server) 686 | .post('/') 687 | .set('Content-Type', 'application/x-www-form-urlencoded') 688 | .send(' user=tobi') 689 | .expect(403, '[entity.verify.failed] no leading space', done) 690 | }) 691 | 692 | it('should allow custom codes', function (done) { 693 | var server = createServer({ 694 | verify: function (req, res, buf) { 695 | if (buf[0] !== 0x20) return 696 | var err = new Error('no leading space') 697 | err.status = 400 698 | throw err 699 | } 700 | }) 701 | 702 | request(server) 703 | .post('/') 704 | .set('Content-Type', 'application/x-www-form-urlencoded') 705 | .send(' user=tobi') 706 | .expect(400, '[entity.verify.failed] no leading space', done) 707 | }) 708 | 709 | it('should allow custom type', function (done) { 710 | var server = createServer({ 711 | verify: function (req, res, buf) { 712 | if (buf[0] !== 0x20) return 713 | var err = new Error('no leading space') 714 | err.type = 'foo.bar' 715 | throw err 716 | } 717 | }) 718 | 719 | request(server) 720 | .post('/') 721 | .set('Content-Type', 'application/x-www-form-urlencoded') 722 | .send(' user=tobi') 723 | .expect(403, '[foo.bar] no leading space', done) 724 | }) 725 | 726 | it('should allow pass-through', function (done) { 727 | var server = createServer({ 728 | verify: function (req, res, buf) { 729 | if (buf[0] === 0x5b) throw new Error('no arrays') 730 | } 731 | }) 732 | 733 | request(server) 734 | .post('/') 735 | .set('Content-Type', 'application/x-www-form-urlencoded') 736 | .send('user=tobi') 737 | .expect(200, '{"user":"tobi"}', done) 738 | }) 739 | 740 | it('should 415 on unknown charset prior to verify', function (done) { 741 | var server = createServer({ 742 | verify: function (req, res, buf) { 743 | throw new Error('unexpected verify call') 744 | } 745 | }) 746 | 747 | var test = request(server).post('/') 748 | test.set('Content-Type', 'application/x-www-form-urlencoded; charset=x-bogus') 749 | test.write(Buffer.from('00000000', 'hex')) 750 | test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) 751 | }) 752 | }) 753 | 754 | describe('async local storage', function () { 755 | before(function () { 756 | var urlencodedParser = bodyParser.urlencoded() 757 | var store = { foo: 'bar' } 758 | 759 | this.server = createServer(function (req, res, next) { 760 | var asyncLocalStorage = new AsyncLocalStorage() 761 | 762 | asyncLocalStorage.run(store, function () { 763 | urlencodedParser(req, res, function (err) { 764 | var local = asyncLocalStorage.getStore() 765 | 766 | if (local) { 767 | res.setHeader('x-store-foo', String(local.foo)) 768 | } 769 | 770 | next(err) 771 | }) 772 | }) 773 | }) 774 | }) 775 | 776 | it('should presist store', function (done) { 777 | request(this.server) 778 | .post('/') 779 | .set('Content-Type', 'application/x-www-form-urlencoded') 780 | .send('user=tobi') 781 | .expect(200) 782 | .expect('x-store-foo', 'bar') 783 | .expect('{"user":"tobi"}') 784 | .end(done) 785 | }) 786 | 787 | it('should presist store when unmatched content-type', function (done) { 788 | request(this.server) 789 | .post('/') 790 | .set('Content-Type', 'application/fizzbuzz') 791 | .send('buzz') 792 | .expect(200) 793 | .expect('x-store-foo', 'bar') 794 | .expect('undefined') 795 | .end(done) 796 | }) 797 | 798 | it('should presist store when inflated', function (done) { 799 | var test = request(this.server).post('/') 800 | test.set('Content-Encoding', 'gzip') 801 | test.set('Content-Type', 'application/x-www-form-urlencoded') 802 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) 803 | test.expect(200) 804 | test.expect('x-store-foo', 'bar') 805 | test.expect('{"name":"论"}') 806 | test.end(done) 807 | }) 808 | 809 | it('should presist store when inflate error', function (done) { 810 | var test = request(this.server).post('/') 811 | test.set('Content-Encoding', 'gzip') 812 | test.set('Content-Type', 'application/x-www-form-urlencoded') 813 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex')) 814 | test.expect(400) 815 | test.expect('x-store-foo', 'bar') 816 | test.end(done) 817 | }) 818 | 819 | it('should presist store when limit exceeded', function (done) { 820 | request(this.server) 821 | .post('/') 822 | .set('Content-Type', 'application/x-www-form-urlencoded') 823 | .send('user=' + Buffer.alloc(1024 * 100, '.').toString()) 824 | .expect(413) 825 | .expect('x-store-foo', 'bar') 826 | .end(done) 827 | }) 828 | }) 829 | 830 | describe('charset', function () { 831 | before(function () { 832 | this.server = createServer() 833 | }) 834 | 835 | it('should parse utf-8', function (done) { 836 | var test = request(this.server).post('/') 837 | test.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8') 838 | test.write(Buffer.from('6e616d653de8aeba', 'hex')) 839 | test.expect(200, '{"name":"论"}', done) 840 | }) 841 | 842 | it('should parse when content-length != char length', function (done) { 843 | var test = request(this.server).post('/') 844 | test.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8') 845 | test.set('Content-Length', '7') 846 | test.write(Buffer.from('746573743dc3a5', 'hex')) 847 | test.expect(200, '{"test":"å"}', done) 848 | }) 849 | 850 | it('should default to utf-8', function (done) { 851 | var test = request(this.server).post('/') 852 | test.set('Content-Type', 'application/x-www-form-urlencoded') 853 | test.write(Buffer.from('6e616d653de8aeba', 'hex')) 854 | test.expect(200, '{"name":"论"}', done) 855 | }) 856 | 857 | it('should fail on unknown charset', function (done) { 858 | var test = request(this.server).post('/') 859 | test.set('Content-Type', 'application/x-www-form-urlencoded; charset=koi8-r') 860 | test.write(Buffer.from('6e616d653dcec5d4', 'hex')) 861 | test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done) 862 | }) 863 | }) 864 | 865 | describe('encoding', function () { 866 | before(function () { 867 | this.server = createServer({ limit: '10kb' }) 868 | }) 869 | 870 | it('should parse without encoding', function (done) { 871 | var test = request(this.server).post('/') 872 | test.set('Content-Type', 'application/x-www-form-urlencoded') 873 | test.write(Buffer.from('6e616d653de8aeba', 'hex')) 874 | test.expect(200, '{"name":"论"}', done) 875 | }) 876 | 877 | it('should support identity encoding', function (done) { 878 | var test = request(this.server).post('/') 879 | test.set('Content-Encoding', 'identity') 880 | test.set('Content-Type', 'application/x-www-form-urlencoded') 881 | test.write(Buffer.from('6e616d653de8aeba', 'hex')) 882 | test.expect(200, '{"name":"论"}', done) 883 | }) 884 | 885 | it('should support gzip encoding', function (done) { 886 | var test = request(this.server).post('/') 887 | test.set('Content-Encoding', 'gzip') 888 | test.set('Content-Type', 'application/x-www-form-urlencoded') 889 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) 890 | test.expect(200, '{"name":"论"}', done) 891 | }) 892 | 893 | it('should support deflate encoding', function (done) { 894 | var test = request(this.server).post('/') 895 | test.set('Content-Encoding', 'deflate') 896 | test.set('Content-Type', 'application/x-www-form-urlencoded') 897 | test.write(Buffer.from('789ccb4bcc4db57db16e17001068042f', 'hex')) 898 | test.expect(200, '{"name":"论"}', done) 899 | }) 900 | 901 | it('should support brotli encoding', function (done) { 902 | var test = request(this.server).post('/') 903 | test.set('Content-Encoding', 'br') 904 | test.set('Content-Type', 'application/x-www-form-urlencoded') 905 | test.write(Buffer.from('8b03806e616d653de8aeba03', 'hex')) 906 | test.expect(200, '{"name":"论"}', done) 907 | }) 908 | 909 | it('should be case-insensitive', function (done) { 910 | var test = request(this.server).post('/') 911 | test.set('Content-Encoding', 'GZIP') 912 | test.set('Content-Type', 'application/x-www-form-urlencoded') 913 | test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) 914 | test.expect(200, '{"name":"论"}', done) 915 | }) 916 | 917 | it('should 415 on unknown encoding', function (done) { 918 | var test = request(this.server).post('/') 919 | test.set('Content-Encoding', 'nulls') 920 | test.set('Content-Type', 'application/x-www-form-urlencoded') 921 | test.write(Buffer.from('000000000000', 'hex')) 922 | test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) 923 | }) 924 | }) 925 | }) 926 | 927 | function createManyParams (count) { 928 | var str = '' 929 | 930 | if (count === 0) { 931 | return str 932 | } 933 | 934 | str += '0=0' 935 | 936 | for (var i = 1; i < count; i++) { 937 | var n = i.toString(36) 938 | str += '&' + n + '=' + n 939 | } 940 | 941 | return str 942 | } 943 | 944 | function createServer (opts) { 945 | var _bodyParser = typeof opts !== 'function' 946 | ? bodyParser.urlencoded(opts) 947 | : opts 948 | 949 | return http.createServer(function (req, res) { 950 | _bodyParser(req, res, function (err) { 951 | if (err) { 952 | res.statusCode = err.status || 500 953 | res.end('[' + err.type + '] ' + err.message) 954 | } else { 955 | res.statusCode = 200 956 | res.end(JSON.stringify(req.body) || typeof req.body) 957 | } 958 | }) 959 | }) 960 | } 961 | 962 | function expectKeyCount (count) { 963 | return function (res) { 964 | assert.strictEqual(Object.keys(JSON.parse(res.text)).length, count) 965 | } 966 | } 967 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('node:assert') 4 | const { normalizeOptions } = require('../lib/utils.js') 5 | 6 | describe('normalizeOptions(options, defaultType)', () => { 7 | it('should return default options when no options are provided', () => { 8 | for (const options of [undefined, null, {}]) { 9 | const result = normalizeOptions(options, 'application/json') 10 | assert.strictEqual(result.inflate, true) 11 | assert.strictEqual(result.limit, 100 * 1024) // 100kb in bytes 12 | assert.strictEqual(result.verify, false) 13 | assert.strictEqual(typeof result.shouldParse, 'function') 14 | } 15 | }) 16 | 17 | it('should override default options with provided options', () => { 18 | const options = { 19 | inflate: false, 20 | limit: '200kb', 21 | type: 'application/xml', 22 | verify: () => {} 23 | } 24 | const result = normalizeOptions(options, 'application/json') 25 | assert.strictEqual(result.inflate, false) 26 | assert.strictEqual(result.limit, 200 * 1024) // 200kb in bytes 27 | assert.strictEqual(result.verify, options.verify) 28 | assert.strictEqual(typeof result.shouldParse, 'function') 29 | }) 30 | 31 | it('should remove additional options', () => { 32 | const options = { 33 | inflate: false, 34 | limit: '200kb', 35 | type: 'application/xml', 36 | verify: () => {}, 37 | additional: 'option', 38 | something: 'weird' 39 | } 40 | const result = normalizeOptions(options, 'application/json') 41 | assert.strictEqual(result.inflate, false) 42 | assert.strictEqual(result.limit, 200 * 1024) // 200kb in bytes 43 | assert.strictEqual(result.verify, options.verify) 44 | assert.strictEqual(typeof result.shouldParse, 'function') 45 | assert.strictEqual(result.additional, undefined) 46 | assert.strictEqual(result.something, undefined) 47 | }) 48 | 49 | describe('options', () => { 50 | describe('verify', () => { 51 | it('should throw an error if verify is not a function', () => { 52 | assert.throws(() => { 53 | normalizeOptions({ verify: 'not a function' }, 'application/json') 54 | }, /option verify must be function/) 55 | }) 56 | 57 | it('should accept a verify function', () => { 58 | const verify = () => {} 59 | const result = normalizeOptions({ verify }, 'application/json') 60 | assert.strictEqual(result.verify, verify) 61 | }) 62 | }) 63 | 64 | describe('limit', () => { 65 | it('should return the default limit if limit is not provided', () => { 66 | const result = normalizeOptions({}, 'application/json') 67 | assert.strictEqual(result.limit, 100 * 1024) // 100kb in bytes 68 | }) 69 | 70 | it('should accept a number limit', () => { 71 | const result = normalizeOptions({ limit: 1234 }, 'application/json') 72 | assert.strictEqual(result.limit, 1234) 73 | }) 74 | 75 | it('should parse a string limit', () => { 76 | const result = normalizeOptions({ limit: '200kb' }, 'application/json') 77 | assert.strictEqual(result.limit, 200 * 1024) // 200kb in bytes 78 | }) 79 | 80 | it('should return null for an invalid limit', () => { 81 | const result = normalizeOptions({ limit: 'invalid' }, 'application/json') 82 | assert.strictEqual(result.limit, null) 83 | }) 84 | }) 85 | 86 | describe('type', () => { 87 | it('should return the default type if type is not provided', () => { 88 | const result = normalizeOptions({}, 'application/json') 89 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/json', 'content-length': '1024' } }), true) 90 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/xml', 'content-length': '1024' } }), false) 91 | }) 92 | 93 | it('should accept a string type', () => { 94 | const result = normalizeOptions({ type: 'application/xml' }, 'application/json') 95 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/xml', 'content-length': '1024' } }), true) 96 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/json', 'content-length': '1024' } }), false) 97 | }) 98 | 99 | it('should accept an array of strings type', () => { 100 | const result = normalizeOptions({ type: ['application/xml', 'application/*+json'] }, 'application/json') 101 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/xml', 'content-length': '1024' } }), true) 102 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/ld+json', 'content-length': '1024' } }), true) 103 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/json', 'content-length': '1024' } }), false) 104 | }) 105 | 106 | it('should accept a type checking function', () => { 107 | const result = normalizeOptions({ type: () => true }, 'application/json') 108 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/xml' } }), true) 109 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/json' } }), true) 110 | }) 111 | }) 112 | }) 113 | 114 | describe('defaultType', () => { 115 | it('should throw an error if defaultType is not provided', () => { 116 | assert.throws(() => { 117 | normalizeOptions({}) 118 | }, /defaultType must be provided/) 119 | assert.throws(() => { 120 | normalizeOptions({}, undefined) 121 | }, /defaultType must be provided/) 122 | }) 123 | 124 | it('should convert string defaultType to a request content-type checking function', () => { 125 | const result = normalizeOptions({}, 'application/json') 126 | assert.strictEqual(typeof result.shouldParse, 'function') 127 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/json', 'content-length': '1024' } }), true) 128 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/xml', 'content-length': '100' } }), false) 129 | }) 130 | 131 | it('should convert array of strings defaultType to a request content-type checking function', () => { 132 | const result = normalizeOptions({}, ['application/json', 'application/*+json']) 133 | assert.strictEqual(typeof result.shouldParse, 'function') 134 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/json', 'content-length': '1024' } }), true) 135 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/ld+json', 'content-length': '1024' } }), true) 136 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/xml', 'content-length': '100' } }), false) 137 | }) 138 | 139 | it('should use function defaultType directly as the request content-type checker', () => { 140 | const typeFunction = (req) => req.headers['content-type'].endsWith('+json') 141 | const result = normalizeOptions({}, typeFunction) 142 | assert.strictEqual(result.shouldParse, typeFunction) 143 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/ld+json' } }), true) 144 | assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/xml' } }), false) 145 | }) 146 | }) 147 | }) 148 | --------------------------------------------------------------------------------