├── .eslintignore ├── .eslintrc.yml ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ └── scorecard.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── .eslintrc.yml ├── support ├── sws.js └── utils.js └── test.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 | -------------------------------------------------------------------------------- /.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 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '*.md' 9 | pull_request: 10 | paths-ignore: 11 | - '*.md' 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | name: Lint 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | 23 | - name: Install Node.js 24 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 25 | with: 26 | node-version: 'lts/*' 27 | 28 | - name: Install Node.js dependencies 29 | run: npm install --ignore-scripts --include=dev 30 | 31 | - name: Lint code 32 | run: npm run lint 33 | 34 | test: 35 | name: Test - Node.js ${{ matrix.node-version }} 36 | runs-on: ubuntu-latest 37 | strategy: 38 | matrix: 39 | node-version: [18, 19, 20, 21, 22, 23, 24] 40 | 41 | steps: 42 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 43 | 44 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | check-latest: true 48 | 49 | - name: Install Node.js dependencies 50 | run: npm install 51 | 52 | - name: Run tests 53 | run: npm run test-ci 54 | 55 | - name: Upload code coverage 56 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 57 | with: 58 | name: coverage-node-${{ matrix.node-version }} 59 | path: ./coverage/lcov.info 60 | retention-days: 1 61 | 62 | coverage: 63 | needs: test 64 | runs-on: ubuntu-latest 65 | permissions: 66 | contents: read 67 | checks: write 68 | steps: 69 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 70 | 71 | - name: Install lcov 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 | run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info 82 | 83 | - name: Upload coverage report 84 | uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 85 | with: 86 | file: ./lcov.info 87 | -------------------------------------------------------------------------------- /.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 72 | -------------------------------------------------------------------------------- /.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 | 7 | on: 8 | # For Branch-Protection check. Only the default branch is supported. See 9 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 10 | branch_protection_rule: 11 | # To guarantee Maintained check is occasionally updated. See 12 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 13 | schedule: 14 | - cron: '16 21 * * 1' 15 | push: 16 | branches: [ "master" ] 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analysis: 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # Needed to upload the results to code-scanning dashboard. 27 | security-events: write 28 | # Needed to publish results and get a badge (see publish_results below). 29 | id-token: write 30 | # Uncomment the permissions below if installing in a private repository. 31 | # contents: read 32 | # actions: read 33 | 34 | steps: 35 | - name: "Checkout code" 36 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | with: 38 | persist-credentials: false 39 | 40 | - name: "Run analysis" 41 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 42 | with: 43 | results_file: results.sarif 44 | results_format: sarif 45 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 46 | # - you want to enable the Branch-Protection check on a *public* repository, or 47 | # - you are installing Scorecard on a *private* repository 48 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 49 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 50 | 51 | # Public repositories: 52 | # - Publish results to OpenSSF REST API for easy access by consumers 53 | # - Allows the repository to include the Scorecard badge. 54 | # - See https://github.com/ossf/scorecard-action#publishing-results. 55 | # For private repositories: 56 | # - `publish_results` will always be set to `false`, regardless 57 | # of the value entered here. 58 | publish_results: true 59 | 60 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 61 | # format to the repository Actions tab. 62 | - name: "Upload artifact" 63 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 64 | with: 65 | name: SARIF file 66 | path: results.sarif 67 | retention-days: 5 68 | 69 | # Upload the results to GitHub's code scanning dashboard. 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | v2.1.0 / 2025-03-05 2 | ================== 3 | 4 | * deps: 5 | * use caret notation for dependency versions 6 | * encodeurl@^2.0.0 7 | * debug@^4.4.0 8 | * remove `ServerResponse.headersSent` support check 9 | * remove setImmediate support check 10 | * update test dependencies 11 | * remove unnecessary devDependency `safe-buffer` 12 | * remove `unpipe` package and use native `unpipe()` method 13 | * remove unnecessary devDependency `readable-stream` 14 | * refactor: use object spread to copy error headers 15 | * refactor: use replaceAll instead of replace with a regex 16 | * refactor: replace setHeaders function with optimized inline header setting 17 | 18 | v2.0.0 / 2024-09-02 19 | ================== 20 | 21 | * drop support for node <18 22 | * ignore status message for HTTP/2 (#53) 23 | 24 | v1.3.1 / 2024-09-11 25 | ================== 26 | 27 | * deps: encodeurl@~2.0.0 28 | 29 | v1.3.0 / 2024-09-03 30 | ================== 31 | 32 | * ignore status message for HTTP/2 (#53) 33 | 34 | v1.2.1 / 2024-09-02 35 | ================== 36 | 37 | * Gracefully handle when handling an error and socket is null 38 | 39 | 1.2.0 / 2022-03-22 40 | ================== 41 | 42 | * Remove set content headers that break response 43 | * deps: on-finished@2.4.1 44 | * deps: statuses@2.0.1 45 | - Rename `425 Unordered Collection` to standard `425 Too Early` 46 | 47 | 1.1.2 / 2019-05-09 48 | ================== 49 | 50 | * Set stricter `Content-Security-Policy` header 51 | * deps: parseurl@~1.3.3 52 | * deps: statuses@~1.5.0 53 | 54 | 1.1.1 / 2018-03-06 55 | ================== 56 | 57 | * Fix 404 output for bad / missing pathnames 58 | * deps: encodeurl@~1.0.2 59 | - Fix encoding `%` as last character 60 | * deps: statuses@~1.4.0 61 | 62 | 1.1.0 / 2017-09-24 63 | ================== 64 | 65 | * Use `res.headersSent` when available 66 | 67 | 1.0.6 / 2017-09-22 68 | ================== 69 | 70 | * deps: debug@2.6.9 71 | 72 | 1.0.5 / 2017-09-15 73 | ================== 74 | 75 | * deps: parseurl@~1.3.2 76 | - perf: reduce overhead for full URLs 77 | - perf: unroll the "fast-path" `RegExp` 78 | 79 | 1.0.4 / 2017-08-03 80 | ================== 81 | 82 | * deps: debug@2.6.8 83 | 84 | 1.0.3 / 2017-05-16 85 | ================== 86 | 87 | * deps: debug@2.6.7 88 | - deps: ms@2.0.0 89 | 90 | 1.0.2 / 2017-04-22 91 | ================== 92 | 93 | * deps: debug@2.6.4 94 | - deps: ms@0.7.3 95 | 96 | 1.0.1 / 2017-03-21 97 | ================== 98 | 99 | * Fix missing `` in HTML document 100 | * deps: debug@2.6.3 101 | - Fix: `DEBUG_MAX_ARRAY_LENGTH` 102 | 103 | 1.0.0 / 2017-02-15 104 | ================== 105 | 106 | * Fix exception when `err` cannot be converted to a string 107 | * Fully URL-encode the pathname in the 404 message 108 | * Only include the pathname in the 404 message 109 | * Send complete HTML document 110 | * Set `Content-Security-Policy: default-src 'self'` header 111 | * deps: debug@2.6.1 112 | - Allow colors in workers 113 | - Deprecated `DEBUG_FD` environment variable set to `3` or higher 114 | - Fix error when running under React Native 115 | - Use same color for same namespace 116 | - deps: ms@0.7.2 117 | 118 | 0.5.1 / 2016-11-12 119 | ================== 120 | 121 | * Fix exception when `err.headers` is not an object 122 | * deps: statuses@~1.3.1 123 | * perf: hoist regular expressions 124 | * perf: remove duplicate validation path 125 | 126 | 0.5.0 / 2016-06-15 127 | ================== 128 | 129 | * Change invalid or non-numeric status code to 500 130 | * Overwrite status message to match set status code 131 | * Prefer `err.statusCode` if `err.status` is invalid 132 | * Set response headers from `err.headers` object 133 | * Use `statuses` instead of `http` module for status messages 134 | - Includes all defined status messages 135 | 136 | 0.4.1 / 2015-12-02 137 | ================== 138 | 139 | * deps: escape-html@~1.0.3 140 | - perf: enable strict mode 141 | - perf: optimize string replacement 142 | - perf: use faster string coercion 143 | 144 | 0.4.0 / 2015-06-14 145 | ================== 146 | 147 | * Fix a false-positive when unpiping in Node.js 0.8 148 | * Support `statusCode` property on `Error` objects 149 | * Use `unpipe` module for unpiping requests 150 | * deps: escape-html@1.0.2 151 | * deps: on-finished@~2.3.0 152 | - Add defined behavior for HTTP `CONNECT` requests 153 | - Add defined behavior for HTTP `Upgrade` requests 154 | - deps: ee-first@1.1.1 155 | * perf: enable strict mode 156 | * perf: remove argument reassignment 157 | 158 | 0.3.6 / 2015-05-11 159 | ================== 160 | 161 | * deps: debug@~2.2.0 162 | - deps: ms@0.7.1 163 | 164 | 0.3.5 / 2015-04-22 165 | ================== 166 | 167 | * deps: on-finished@~2.2.1 168 | - Fix `isFinished(req)` when data buffered 169 | 170 | 0.3.4 / 2015-03-15 171 | ================== 172 | 173 | * deps: debug@~2.1.3 174 | - Fix high intensity foreground color for bold 175 | - deps: ms@0.7.0 176 | 177 | 0.3.3 / 2015-01-01 178 | ================== 179 | 180 | * deps: debug@~2.1.1 181 | * deps: on-finished@~2.2.0 182 | 183 | 0.3.2 / 2014-10-22 184 | ================== 185 | 186 | * deps: on-finished@~2.1.1 187 | - Fix handling of pipelined requests 188 | 189 | 0.3.1 / 2014-10-16 190 | ================== 191 | 192 | * deps: debug@~2.1.0 193 | - Implement `DEBUG_FD` env variable support 194 | 195 | 0.3.0 / 2014-09-17 196 | ================== 197 | 198 | * Terminate in progress response only on error 199 | * Use `on-finished` to determine request status 200 | 201 | 0.2.0 / 2014-09-03 202 | ================== 203 | 204 | * Set `X-Content-Type-Options: nosniff` header 205 | * deps: debug@~2.0.0 206 | 207 | 0.1.0 / 2014-07-16 208 | ================== 209 | 210 | * Respond after request fully read 211 | - prevents hung responses and socket hang ups 212 | * deps: debug@1.0.4 213 | 214 | 0.0.3 / 2014-07-11 215 | ================== 216 | 217 | * deps: debug@1.0.3 218 | - Add support for multiple wildcards in namespaces 219 | 220 | 0.0.2 / 2014-06-19 221 | ================== 222 | 223 | * Handle invalid status codes 224 | 225 | 0.0.1 / 2014-06-05 226 | ================== 227 | 228 | * deps: debug@1.0.2 229 | 230 | 0.0.0 / 2014-06-05 231 | ================== 232 | 233 | * Extracted from connect/express 234 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014-2022 Douglas Christopher Wilson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # finalhandler 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![NPM Downloads][downloads-image]][downloads-url] 5 | [![Node.js Version][node-image]][node-url] 6 | [![Build Status][github-actions-ci-image]][github-actions-ci-url] 7 | [![Test Coverage][coveralls-image]][coveralls-url] 8 | [![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] 9 | 10 | Node.js function to invoke as the final step to respond to HTTP request. 11 | 12 | ## Installation 13 | 14 | This is a [Node.js](https://nodejs.org/en/) module available through the 15 | [npm registry](https://www.npmjs.com/). Installation is done using the 16 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 17 | 18 | ```sh 19 | $ npm install finalhandler 20 | ``` 21 | 22 | ## API 23 | 24 | ```js 25 | const finalhandler = require('finalhandler') 26 | ``` 27 | 28 | ### finalhandler(req, res, [options]) 29 | 30 | Returns function to be invoked as the final step for the given `req` and `res`. 31 | This function is to be invoked as `fn(err)`. If `err` is falsy, the handler will 32 | write out a 404 response to the `res`. If it is truthy, an error response will 33 | be written out to the `res` or `res` will be terminated if a response has already 34 | started. 35 | 36 | When an error is written, the following information is added to the response: 37 | 38 | * The `res.statusCode` is set from `err.status` (or `err.statusCode`). If 39 | this value is outside the 4xx or 5xx range, it will be set to 500. 40 | * The `res.statusMessage` is set according to the status code. 41 | * The body will be the HTML of the status code message if `env` is 42 | `'production'`, otherwise will be `err.stack`. 43 | * Any headers specified in an `err.headers` object. 44 | 45 | The final handler will also unpipe anything from `req` when it is invoked. 46 | 47 | #### options.env 48 | 49 | By default, the environment is determined by `NODE_ENV` variable, but it can be 50 | overridden by this option. 51 | 52 | #### options.onerror 53 | 54 | Provide a function to be called with the `err` when it exists. Can be used for 55 | writing errors to a central location without excessive function generation. Called 56 | as `onerror(err, req, res)`. 57 | 58 | ## Examples 59 | 60 | ### always 404 61 | 62 | ```js 63 | const finalhandler = require('finalhandler') 64 | const http = require('http') 65 | 66 | const server = http.createServer((req, res) => { 67 | const done = finalhandler(req, res) 68 | done() 69 | }) 70 | 71 | server.listen(3000) 72 | ``` 73 | 74 | ### perform simple action 75 | 76 | ```js 77 | const finalhandler = require('finalhandler') 78 | const fs = require('fs') 79 | const http = require('http') 80 | 81 | const server = http.createServer((req, res) => { 82 | const done = finalhandler(req, res) 83 | 84 | fs.readFile('index.html', (err, buf) => { 85 | if (err) return done(err) 86 | res.setHeader('Content-Type', 'text/html') 87 | res.end(buf) 88 | }) 89 | }) 90 | 91 | server.listen(3000) 92 | ``` 93 | 94 | ### use with middleware-style functions 95 | 96 | ```js 97 | const finalhandler = require('finalhandler') 98 | const http = require('http') 99 | const serveStatic = require('serve-static') 100 | 101 | const serve = serveStatic('public') 102 | 103 | const server = http.createServer((req, res) => { 104 | const done = finalhandler(req, res) 105 | serve(req, res, done) 106 | }) 107 | 108 | server.listen(3000) 109 | ``` 110 | 111 | ### keep log of all errors 112 | 113 | ```js 114 | const finalhandler = require('finalhandler') 115 | const fs = require('fs') 116 | const http = require('http') 117 | 118 | const server = http.createServer((req, res) => { 119 | const done = finalhandler(req, res, { onerror: logerror }) 120 | 121 | fs.readFile('index.html', (err, buf) => { 122 | if (err) return done(err) 123 | res.setHeader('Content-Type', 'text/html') 124 | res.end(buf) 125 | }) 126 | }) 127 | 128 | server.listen(3000) 129 | 130 | function logerror (err) { 131 | console.error(err.stack || err.toString()) 132 | } 133 | ``` 134 | 135 | ## License 136 | 137 | [MIT](LICENSE) 138 | 139 | [npm-image]: https://img.shields.io/npm/v/finalhandler.svg 140 | [npm-url]: https://npmjs.org/package/finalhandler 141 | [node-image]: https://img.shields.io/node/v/finalhandler.svg 142 | [node-url]: https://nodejs.org/en/download 143 | [coveralls-image]: https://img.shields.io/coveralls/pillarjs/finalhandler.svg 144 | [coveralls-url]: https://coveralls.io/r/pillarjs/finalhandler?branch=master 145 | [downloads-image]: https://img.shields.io/npm/dm/finalhandler.svg 146 | [downloads-url]: https://npmjs.org/package/finalhandler 147 | [github-actions-ci-image]: https://github.com/pillarjs/finalhandler/actions/workflows/ci.yml/badge.svg 148 | [github-actions-ci-url]: https://github.com/pillarjs/finalhandler/actions/workflows/ci.yml 149 | [ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/pillarjs/finalhandler/badge 150 | [ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/pillarjs/finalhandler -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * finalhandler 3 | * Copyright(c) 2014-2022 Douglas Christopher Wilson 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * Module dependencies. 11 | * @private 12 | */ 13 | 14 | var debug = require('debug')('finalhandler') 15 | var encodeUrl = require('encodeurl') 16 | var escapeHtml = require('escape-html') 17 | var onFinished = require('on-finished') 18 | var parseUrl = require('parseurl') 19 | var statuses = require('statuses') 20 | 21 | /** 22 | * Module variables. 23 | * @private 24 | */ 25 | 26 | var isFinished = onFinished.isFinished 27 | 28 | /** 29 | * Create a minimal HTML document. 30 | * 31 | * @param {string} message 32 | * @private 33 | */ 34 | 35 | function createHtmlDocument (message) { 36 | var body = escapeHtml(message) 37 | .replaceAll('\n', '
') 38 | .replaceAll(' ', '  ') 39 | 40 | return '\n' + 41 | '\n' + 42 | '\n' + 43 | '\n' + 44 | 'Error\n' + 45 | '\n' + 46 | '\n' + 47 | '
' + body + '
\n' + 48 | '\n' + 49 | '\n' 50 | } 51 | 52 | /** 53 | * Module exports. 54 | * @public 55 | */ 56 | 57 | module.exports = finalhandler 58 | 59 | /** 60 | * Create a function to handle the final response. 61 | * 62 | * @param {Request} req 63 | * @param {Response} res 64 | * @param {Object} [options] 65 | * @return {Function} 66 | * @public 67 | */ 68 | 69 | function finalhandler (req, res, options) { 70 | var opts = options || {} 71 | 72 | // get environment 73 | var env = opts.env || process.env.NODE_ENV || 'development' 74 | 75 | // get error callback 76 | var onerror = opts.onerror 77 | 78 | return function (err) { 79 | var headers 80 | var msg 81 | var status 82 | 83 | // ignore 404 on in-flight response 84 | if (!err && res.headersSent) { 85 | debug('cannot 404 after headers sent') 86 | return 87 | } 88 | 89 | // unhandled error 90 | if (err) { 91 | // respect status code from error 92 | status = getErrorStatusCode(err) 93 | 94 | if (status === undefined) { 95 | // fallback to status code on response 96 | status = getResponseStatusCode(res) 97 | } else { 98 | // respect headers from error 99 | headers = getErrorHeaders(err) 100 | } 101 | 102 | // get error message 103 | msg = getErrorMessage(err, status, env) 104 | } else { 105 | // not found 106 | status = 404 107 | msg = 'Cannot ' + req.method + ' ' + encodeUrl(getResourceName(req)) 108 | } 109 | 110 | debug('default %s', status) 111 | 112 | // schedule onerror callback 113 | if (err && onerror) { 114 | setImmediate(onerror, err, req, res) 115 | } 116 | 117 | // cannot actually respond 118 | if (res.headersSent) { 119 | debug('cannot %d after headers sent', status) 120 | if (req.socket) { 121 | req.socket.destroy() 122 | } 123 | return 124 | } 125 | 126 | // send response 127 | send(req, res, status, headers, msg) 128 | } 129 | } 130 | 131 | /** 132 | * Get headers from Error object. 133 | * 134 | * @param {Error} err 135 | * @return {object} 136 | * @private 137 | */ 138 | 139 | function getErrorHeaders (err) { 140 | if (!err.headers || typeof err.headers !== 'object') { 141 | return undefined 142 | } 143 | 144 | return { ...err.headers } 145 | } 146 | 147 | /** 148 | * Get message from Error object, fallback to status message. 149 | * 150 | * @param {Error} err 151 | * @param {number} status 152 | * @param {string} env 153 | * @return {string} 154 | * @private 155 | */ 156 | 157 | function getErrorMessage (err, status, env) { 158 | var msg 159 | 160 | if (env !== 'production') { 161 | // use err.stack, which typically includes err.message 162 | msg = err.stack 163 | 164 | // fallback to err.toString() when possible 165 | if (!msg && typeof err.toString === 'function') { 166 | msg = err.toString() 167 | } 168 | } 169 | 170 | return msg || statuses.message[status] 171 | } 172 | 173 | /** 174 | * Get status code from Error object. 175 | * 176 | * @param {Error} err 177 | * @return {number} 178 | * @private 179 | */ 180 | 181 | function getErrorStatusCode (err) { 182 | // check err.status 183 | if (typeof err.status === 'number' && err.status >= 400 && err.status < 600) { 184 | return err.status 185 | } 186 | 187 | // check err.statusCode 188 | if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 600) { 189 | return err.statusCode 190 | } 191 | 192 | return undefined 193 | } 194 | 195 | /** 196 | * Get resource name for the request. 197 | * 198 | * This is typically just the original pathname of the request 199 | * but will fallback to "resource" is that cannot be determined. 200 | * 201 | * @param {IncomingMessage} req 202 | * @return {string} 203 | * @private 204 | */ 205 | 206 | function getResourceName (req) { 207 | try { 208 | return parseUrl.original(req).pathname 209 | } catch (e) { 210 | return 'resource' 211 | } 212 | } 213 | 214 | /** 215 | * Get status code from response. 216 | * 217 | * @param {OutgoingMessage} res 218 | * @return {number} 219 | * @private 220 | */ 221 | 222 | function getResponseStatusCode (res) { 223 | var status = res.statusCode 224 | 225 | // default status code to 500 if outside valid range 226 | if (typeof status !== 'number' || status < 400 || status > 599) { 227 | status = 500 228 | } 229 | 230 | return status 231 | } 232 | 233 | /** 234 | * Send response. 235 | * 236 | * @param {IncomingMessage} req 237 | * @param {OutgoingMessage} res 238 | * @param {number} status 239 | * @param {object} headers 240 | * @param {string} message 241 | * @private 242 | */ 243 | 244 | function send (req, res, status, headers, message) { 245 | function write () { 246 | // response body 247 | var body = createHtmlDocument(message) 248 | 249 | // response status 250 | res.statusCode = status 251 | 252 | if (req.httpVersionMajor < 2) { 253 | res.statusMessage = statuses.message[status] 254 | } 255 | 256 | // remove any content headers 257 | res.removeHeader('Content-Encoding') 258 | res.removeHeader('Content-Language') 259 | res.removeHeader('Content-Range') 260 | 261 | // response headers 262 | for (const [key, value] of Object.entries(headers ?? {})) { 263 | res.setHeader(key, value) 264 | } 265 | 266 | // security headers 267 | res.setHeader('Content-Security-Policy', "default-src 'none'") 268 | res.setHeader('X-Content-Type-Options', 'nosniff') 269 | 270 | // standard headers 271 | res.setHeader('Content-Type', 'text/html; charset=utf-8') 272 | res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8')) 273 | 274 | if (req.method === 'HEAD') { 275 | res.end() 276 | return 277 | } 278 | 279 | res.end(body, 'utf8') 280 | } 281 | 282 | if (isFinished(req)) { 283 | write() 284 | return 285 | } 286 | 287 | // unpipe everything from the request 288 | req.unpipe() 289 | 290 | // flush the request 291 | onFinished(req, write) 292 | req.resume() 293 | } 294 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "finalhandler", 3 | "description": "Node.js final http responder", 4 | "version": "2.1.0", 5 | "author": "Douglas Christopher Wilson ", 6 | "license": "MIT", 7 | "repository": "pillarjs/finalhandler", 8 | "dependencies": { 9 | "debug": "^4.4.0", 10 | "encodeurl": "^2.0.0", 11 | "escape-html": "^1.0.3", 12 | "on-finished": "^2.4.1", 13 | "parseurl": "^1.3.3", 14 | "statuses": "^2.0.1" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^7.32.0", 18 | "eslint-config-standard": "^14.1.1", 19 | "eslint-plugin-import": "^2.31.0", 20 | "eslint-plugin-markdown": "^2.2.1", 21 | "eslint-plugin-node": "^11.1.0", 22 | "eslint-plugin-promise": "^5.2.0", 23 | "eslint-plugin-standard": "^4.1.0", 24 | "mocha": "^11.0.1", 25 | "nyc": "^17.1.0", 26 | "supertest": "^7.0.0" 27 | }, 28 | "files": [ 29 | "LICENSE", 30 | "HISTORY.md", 31 | "index.js" 32 | ], 33 | "engines": { 34 | "node": ">= 0.8" 35 | }, 36 | "scripts": { 37 | "lint": "eslint .", 38 | "test": "mocha --reporter spec --check-leaks test/", 39 | "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", 40 | "test-cov": "nyc --reporter=html --reporter=text npm test", 41 | "test-inspect": "mocha --reporter spec --inspect --inspect-brk test/" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /test/support/sws.js: -------------------------------------------------------------------------------- 1 | var stream = require('stream') 2 | var util = require('util') 3 | 4 | module.exports = SlowWriteStream 5 | 6 | function SlowWriteStream () { 7 | stream.Writable.call(this) 8 | } 9 | 10 | util.inherits(SlowWriteStream, stream.Writable) 11 | 12 | SlowWriteStream.prototype._write = function _write (chunk, encoding, callback) { 13 | setTimeout(callback, 1000) 14 | } 15 | -------------------------------------------------------------------------------- /test/support/utils.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var finalhandler = require('../..') 3 | var http = require('http') 4 | var http2 = require('http2') 5 | 6 | var request = require('supertest') 7 | var SlowWriteStream = require('./sws') 8 | 9 | exports.assert = assert 10 | exports.createError = createError 11 | exports.createHTTPServer = createHTTPServer 12 | exports.createHTTP2Server = createHTTP2Server 13 | exports.createSlowWriteStream = createSlowWriteStream 14 | exports.rawrequest = rawrequest 15 | exports.rawrequestHTTP2 = rawrequestHTTP2 16 | exports.request = request 17 | exports.shouldHaveStatusMessage = shouldHaveStatusMessage 18 | exports.shouldNotHaveBody = shouldNotHaveBody 19 | exports.shouldNotHaveHeader = shouldNotHaveHeader 20 | 21 | function createError (message, props) { 22 | var err = new Error(message) 23 | 24 | if (props) { 25 | for (var prop in props) { 26 | err[prop] = props[prop] 27 | } 28 | } 29 | 30 | return err 31 | } 32 | 33 | function createHTTPServer (err, opts) { 34 | return http.createServer(function (req, res) { 35 | var done = finalhandler(req, res, opts) 36 | 37 | if (typeof err === 'function') { 38 | err(req, res, done) 39 | return 40 | } 41 | 42 | done(err) 43 | }) 44 | } 45 | 46 | function createHTTP2Server (err, opts) { 47 | return http2.createServer(function (req, res) { 48 | var done = finalhandler(req, res, opts) 49 | 50 | if (typeof err === 'function') { 51 | err(req, res, done) 52 | return 53 | } 54 | 55 | done(err) 56 | }) 57 | } 58 | 59 | function createSlowWriteStream () { 60 | return new SlowWriteStream() 61 | } 62 | 63 | function rawrequest (server) { 64 | var _headers = {} 65 | var _path 66 | 67 | function expect (status, body, callback) { 68 | if (arguments.length === 2) { 69 | _headers[status.toLowerCase()] = body 70 | return this 71 | } 72 | 73 | server.listen(function onlisten () { 74 | var addr = this.address() 75 | var port = addr.port 76 | 77 | var req = http.get({ 78 | host: '127.0.0.1', 79 | path: _path, 80 | port: port 81 | }) 82 | req.on('error', callback) 83 | req.on('response', function onresponse (res) { 84 | var buf = '' 85 | 86 | res.setEncoding('utf8') 87 | res.on('data', function ondata (s) { buf += s }) 88 | res.on('end', function onend () { 89 | var err = null 90 | 91 | try { 92 | for (var key in _headers) { 93 | assert.strictEqual(res.headers[key], _headers[key]) 94 | } 95 | 96 | assert.strictEqual(res.statusCode, status) 97 | 98 | if (body instanceof RegExp) { 99 | assert.ok(body.test(buf), 'expected body ' + buf + ' to match ' + body) 100 | } else { 101 | assert.strictEqual(buf, body, 'expected ' + body + ' response body, got ' + buf) 102 | } 103 | } catch (e) { 104 | err = e 105 | } 106 | 107 | server.close() 108 | callback(err) 109 | }) 110 | }) 111 | }) 112 | } 113 | 114 | function get (path) { 115 | _path = path 116 | 117 | return { 118 | expect: expect 119 | } 120 | } 121 | 122 | return { 123 | get: get 124 | } 125 | } 126 | 127 | function rawrequestHTTP2 (server) { 128 | var _headers = {} 129 | var _path 130 | 131 | function expect (status, body, callback) { 132 | if (arguments.length === 2) { 133 | _headers[status.toLowerCase()] = body 134 | return this 135 | } 136 | 137 | server.listen(function onlisten () { 138 | var buf = '' 139 | var resHeaders 140 | var addr = this.address() 141 | var port = addr.port 142 | 143 | var client = http2.connect('http://127.0.0.1:' + port) 144 | var req = client.request({ 145 | ':method': 'GET', 146 | ':path': _path.replace(/http:\/\/localhost/, '') 147 | }) 148 | req.on('error', callback) 149 | req.on('response', function onresponse (responseHeaders) { 150 | resHeaders = responseHeaders 151 | }) 152 | req.on('data', function ondata (s) { buf += s }) 153 | req.on('end', function onend () { 154 | var err = null 155 | 156 | try { 157 | for (var key in _headers) { 158 | assert.strictEqual(resHeaders[key], _headers[key]) 159 | } 160 | 161 | assert.strictEqual(resHeaders[':status'], status) 162 | 163 | if (body instanceof RegExp) { 164 | assert.ok(body.test(buf), 'expected body ' + buf + ' to match ' + body) 165 | } else { 166 | assert.strictEqual(buf, body, 'expected ' + body + ' response body, got ' + buf) 167 | } 168 | } catch (e) { 169 | err = e 170 | } 171 | 172 | req.close() 173 | client.close() 174 | server.close() 175 | callback(err) 176 | }) 177 | }) 178 | } 179 | 180 | function get (path) { 181 | _path = path 182 | 183 | return { 184 | expect: expect 185 | } 186 | } 187 | 188 | return { 189 | get: get 190 | } 191 | } 192 | 193 | function shouldHaveStatusMessage (statusMessage) { 194 | return function (test) { 195 | assert.strictEqual(test.res.statusMessage, statusMessage, 'should have statusMessage "' + statusMessage + '"') 196 | } 197 | } 198 | 199 | function shouldNotHaveBody () { 200 | return function (res) { 201 | assert.ok(res.text === '' || res.text === undefined) 202 | } 203 | } 204 | 205 | function shouldNotHaveHeader (header) { 206 | return function (test) { 207 | assert.ok(test.res.headers[header] === undefined, 'response does not have header "' + header + '"') 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var finalhandler = require('..') 2 | var http = require('http') 3 | 4 | var utils = require('./support/utils') 5 | 6 | var assert = utils.assert 7 | var createError = utils.createError 8 | var createHTTPServer = utils.createHTTPServer 9 | var createHTTP2Server = utils.createHTTP2Server 10 | var createSlowWriteStream = utils.createSlowWriteStream 11 | var rawrequest = utils.rawrequest 12 | var rawrequestHTTP2 = utils.rawrequestHTTP2 13 | var request = utils.request 14 | var shouldHaveStatusMessage = utils.shouldHaveStatusMessage 15 | var shouldNotHaveBody = utils.shouldNotHaveBody 16 | var shouldNotHaveHeader = utils.shouldNotHaveHeader 17 | 18 | var topDescribe = function (type, createServer) { 19 | var wrapper = function wrapper (req) { 20 | if (type === 'http2') { 21 | return req.http2() 22 | } 23 | 24 | return req 25 | } 26 | 27 | describe('headers', function () { 28 | it('should ignore err.headers without status code', function (done) { 29 | wrapper(request(createServer(createError('oops!', { 30 | headers: { 'X-Custom-Header': 'foo' } 31 | }))) 32 | .get('/')) 33 | .expect(shouldNotHaveHeader('X-Custom-Header')) 34 | .expect(500, done) 35 | }) 36 | 37 | it('should ignore err.headers with invalid res.status', function (done) { 38 | wrapper(request(createServer(createError('oops!', { 39 | headers: { 'X-Custom-Header': 'foo' }, 40 | status: 601 41 | }))) 42 | .get('/')) 43 | .expect(shouldNotHaveHeader('X-Custom-Header')) 44 | .expect(500, done) 45 | }) 46 | 47 | it('should ignore err.headers with invalid res.statusCode', function (done) { 48 | wrapper(request(createServer(createError('oops!', { 49 | headers: { 'X-Custom-Header': 'foo' }, 50 | statusCode: 601 51 | }))) 52 | .get('/')) 53 | .expect(shouldNotHaveHeader('X-Custom-Header')) 54 | .expect(500, done) 55 | }) 56 | 57 | it('should include err.headers with err.status', function (done) { 58 | wrapper(request(createServer(createError('oops!', { 59 | headers: { 'X-Custom-Header': 'foo=500', 'X-Custom-Header2': 'bar' }, 60 | status: 500 61 | }))) 62 | .get('/')) 63 | .expect('X-Custom-Header', 'foo=500') 64 | .expect('X-Custom-Header2', 'bar') 65 | .expect(500, done) 66 | }) 67 | 68 | it('should include err.headers with err.statusCode', function (done) { 69 | wrapper(request(createServer(createError('too many requests', { 70 | headers: { 'Retry-After': '5' }, 71 | statusCode: 429 72 | }))) 73 | .get('/')) 74 | .expect('Retry-After', '5') 75 | .expect(429, done) 76 | }) 77 | 78 | it('should ignore err.headers when not an object', function (done) { 79 | wrapper(request(createServer(createError('oops!', { 80 | headers: 'foobar', 81 | statusCode: 500 82 | }))) 83 | .get('/')) 84 | .expect(500, done) 85 | }) 86 | }) 87 | 88 | describe('status code', function () { 89 | it('should 404 on no error', function (done) { 90 | wrapper(request(createServer()) 91 | .get('/')) 92 | .expect(404, done) 93 | }) 94 | 95 | it('should 500 on error', function (done) { 96 | wrapper(request(createServer(createError())) 97 | .get('/')) 98 | .expect(500, done) 99 | }) 100 | 101 | it('should use err.statusCode', function (done) { 102 | wrapper(request(createServer(createError('nope', { 103 | statusCode: 400 104 | }))) 105 | .get('/')) 106 | .expect(400, done) 107 | }) 108 | 109 | it('should ignore non-error err.statusCode code', function (done) { 110 | wrapper(request(createServer(createError('created', { 111 | statusCode: 201 112 | }))) 113 | .get('/')) 114 | .expect(500, done) 115 | }) 116 | 117 | it('should ignore non-numeric err.statusCode', function (done) { 118 | wrapper(request(createServer(createError('oops', { 119 | statusCode: 'oh no' 120 | }))) 121 | .get('/')) 122 | .expect(500, done) 123 | }) 124 | 125 | it('should use err.status', function (done) { 126 | wrapper(request(createServer(createError('nope', { 127 | status: 400 128 | }))) 129 | .get('/')) 130 | .expect(400, done) 131 | }) 132 | 133 | it('should use err.status over err.statusCode', function (done) { 134 | wrapper(request(createServer(createError('nope', { 135 | status: 400, 136 | statusCode: 401 137 | }))) 138 | .get('/')) 139 | .expect(400, done) 140 | }) 141 | 142 | it('should set status to 500 when err.status < 400', function (done) { 143 | wrapper(request(createServer(createError('oops', { 144 | status: 202 145 | }))) 146 | .get('/')) 147 | .expect(500, done) 148 | }) 149 | 150 | it('should set status to 500 when err.status > 599', function (done) { 151 | wrapper(request(createServer(createError('oops', { 152 | status: 601 153 | }))) 154 | .get('/')) 155 | .expect(500, done) 156 | }) 157 | 158 | it('should use err.statusCode over invalid err.status', function (done) { 159 | wrapper(request(createServer(createError('nope', { 160 | status: 50, 161 | statusCode: 410 162 | }))) 163 | .get('/')) 164 | .expect(410, done) 165 | }) 166 | 167 | it('should ignore non-error err.status code', function (done) { 168 | wrapper(request(createServer(createError('created', { 169 | status: 201 170 | }))) 171 | .get('/')) 172 | .expect(500, done) 173 | }) 174 | 175 | it('should ignore non-numeric err.status', function (done) { 176 | wrapper(request(createServer(createError('oops', { 177 | status: 'oh no' 178 | }))) 179 | .get('/')) 180 | .expect(500, done) 181 | }) 182 | }) 183 | 184 | // http2 does not support status message 185 | var describeStatusMessage = !/statusMessage/.test(http.IncomingMessage.toString()) || type === 'http2' 186 | ? describe.skip 187 | : describe 188 | 189 | describeStatusMessage('status message', function () { 190 | it('should be "Not Found" on no error', function (done) { 191 | request(createServer()) 192 | .get('/') 193 | .expect(shouldHaveStatusMessage('Not Found')) 194 | .expect(404, done) 195 | }) 196 | 197 | it('should be "Internal Server Error" on error', function (done) { 198 | request(createServer(createError())) 199 | .get('/') 200 | .expect(shouldHaveStatusMessage('Internal Server Error')) 201 | .expect(500, done) 202 | }) 203 | 204 | it('should be "Bad Request" when err.statusCode = 400', function (done) { 205 | request(createServer(createError('oops', { 206 | status: 400 207 | }))) 208 | .get('/') 209 | .expect(shouldHaveStatusMessage('Bad Request')) 210 | .expect(400, done) 211 | }) 212 | 213 | it('should reset existing res.statusMessage', function (done) { 214 | function onRequest (req, res, next) { 215 | res.statusMessage = 'An Error Occurred' 216 | next(new Error()) 217 | } 218 | 219 | request(createServer(onRequest)) 220 | .get('/') 221 | .expect(shouldHaveStatusMessage('Internal Server Error')) 222 | .expect(500, done) 223 | }) 224 | }) 225 | 226 | describe('404 response', function () { 227 | it('should include method and pathname', function (done) { 228 | wrapper(request(createServer()) 229 | .get('/foo')) 230 | .expect(404, /
Cannot GET \/foo<\/pre>/, done)
231 |     })
232 | 
233 |     it('should escape method and pathname characters', function (done) {
234 |       (type === 'http2' ? rawrequestHTTP2 : rawrequest)(createServer())
235 |         .get('/')
236 |         .expect(404, /
Cannot GET \/%3Cla'me%3E<\/pre>/, done)
237 |     })
238 | 
239 |     it('should fallback to generic pathname without URL', function (done) {
240 |       var server = createServer(function (req, res, next) {
241 |         req.url = undefined
242 |         next()
243 |       })
244 | 
245 |       wrapper(request(server)
246 |         .get('/foo'))
247 |         .expect(404, /
Cannot GET resource<\/pre>/, done)
248 |     })
249 | 
250 |     it('should include original pathname', function (done) {
251 |       var server = createServer(function (req, res, next) {
252 |         var parts = req.url.split('/')
253 |         req.originalUrl = req.url
254 |         req.url = '/' + parts.slice(2).join('/')
255 |         next()
256 |       })
257 | 
258 |       wrapper(request(server)
259 |         .get('/foo/bar'))
260 |         .expect(404, /
Cannot GET \/foo\/bar<\/pre>/, done)
261 |     })
262 | 
263 |     it('should include pathname only', function (done) {
264 |       (type === 'http2' ? rawrequestHTTP2 : rawrequest)(createServer())
265 |         .get('http://localhost/foo?bar=1')
266 |         .expect(404, /
Cannot GET \/foo<\/pre>/, done)
267 |     })
268 | 
269 |     it('should handle HEAD', function (done) {
270 |       wrapper(request(createServer())
271 |         .head('/foo'))
272 |         .expect(404)
273 |         .expect(shouldNotHaveBody())
274 |         .end(done)
275 |     })
276 | 
277 |     it('should include X-Content-Type-Options header', function (done) {
278 |       wrapper(request(createServer())
279 |         .get('/foo'))
280 |         .expect('X-Content-Type-Options', 'nosniff')
281 |         .expect(404, done)
282 |     })
283 | 
284 |     it('should include Content-Security-Policy header', function (done) {
285 |       wrapper(request(createServer())
286 |         .get('/foo'))
287 |         .expect('Content-Security-Policy', "default-src 'none'")
288 |         .expect(404, done)
289 |     })
290 | 
291 |     it('should not hang/error if there is a request body', function (done) {
292 |       var buf = Buffer.alloc(1024 * 16, '.')
293 |       var server = createServer()
294 |       var test = wrapper(request(server).post('/foo'))
295 |       test.write(buf)
296 |       test.write(buf)
297 |       test.write(buf)
298 |       test.expect(404, done)
299 |     })
300 |   })
301 | 
302 |   describe('error response', function () {
303 |     it('should include error stack', function (done) {
304 |       wrapper(request(createServer(createError('boom!')))
305 |         .get('/foo'))
306 |         .expect(500, /
Error: boom!
   at/, done) 307 | }) 308 | 309 | it('should handle HEAD', function (done) { 310 | wrapper(request(createServer(createError('boom!'))) 311 | .head('/foo')) 312 | .expect(500) 313 | .expect(shouldNotHaveBody()) 314 | .end(done) 315 | }) 316 | 317 | it('should include X-Content-Type-Options header', function (done) { 318 | wrapper(request(createServer(createError('boom!'))) 319 | .get('/foo')) 320 | .expect('X-Content-Type-Options', 'nosniff') 321 | .expect(500, done) 322 | }) 323 | 324 | it('should includeContent-Security-Policy header', function (done) { 325 | wrapper(request(createServer(createError('boom!'))) 326 | .get('/foo')) 327 | .expect('Content-Security-Policy', "default-src 'none'") 328 | .expect(500, done) 329 | }) 330 | 331 | it('should handle non-error-objects', function (done) { 332 | wrapper(request(createServer('lame string')) 333 | .get('/foo')) 334 | .expect(500, /
lame string<\/pre>/, done)
335 |     })
336 | 
337 |     it('should handle null prototype objects', function (done) {
338 |       wrapper(request(createServer(Object.create(null)))
339 |         .get('/foo'))
340 |         .expect(500, /
Internal Server Error<\/pre>/, done)
341 |     })
342 | 
343 |     it('should send staus code name when production', function (done) {
344 |       var err = createError('boom!', {
345 |         status: 501
346 |       })
347 |       wrapper(request(createServer(err, {
348 |         env: 'production'
349 |       }))
350 |         .get('/foo'))
351 |         .expect(501, /
Not Implemented<\/pre>/, done)
352 |     })
353 | 
354 |     describe('when there is a request body', function () {
355 |       it('should not hang/error when unread', function (done) {
356 |         var buf = Buffer.alloc(1024 * 16, '.')
357 |         var server = createServer(new Error('boom!'))
358 |         var test = wrapper(request(server).post('/foo'))
359 |         test.write(buf)
360 |         test.write(buf)
361 |         test.write(buf)
362 |         test.expect(500, done)
363 |       })
364 | 
365 |       it('should not hang/error when actively piped', function (done) {
366 |         var buf = Buffer.alloc(1024 * 16, '.')
367 |         var server = createServer(function (req, res, next) {
368 |           req.pipe(stream)
369 |           process.nextTick(function () {
370 |             next(new Error('boom!'))
371 |           })
372 |         })
373 |         var stream = createSlowWriteStream()
374 |         var test = wrapper(request(server).post('/foo'))
375 |         test.write(buf)
376 |         test.write(buf)
377 |         test.write(buf)
378 |         test.expect(500, done)
379 |       })
380 | 
381 |       it('should not hang/error when read', function (done) {
382 |         var buf = Buffer.alloc(1024 * 16, '.')
383 |         var server = createServer(function (req, res, next) {
384 |           // read off the request
385 |           req.once('end', function () {
386 |             next(new Error('boom!'))
387 |           })
388 |           req.resume()
389 |         })
390 |         var test = wrapper(request(server).post('/foo'))
391 |         test.write(buf)
392 |         test.write(buf)
393 |         test.write(buf)
394 |         test.expect(500, done)
395 |       })
396 |     })
397 | 
398 |     describe('when res.statusCode set', function () {
399 |       it('should keep when >= 400', function (done) {
400 |         var server = createServer(function (req, res) {
401 |           var done = finalhandler(req, res)
402 |           res.statusCode = 503
403 |           done(new Error('oops'))
404 |         })
405 | 
406 |         wrapper(request(server)
407 |           .get('/foo'))
408 |           .expect(503, done)
409 |       })
410 | 
411 |       it('should convert to 500 is not a number', function (done) {
412 |         // http2 does not support non numeric status code
413 |         if (type === 'http2') {
414 |           done()
415 |           return
416 |         }
417 | 
418 |         var server = createServer(function (req, res) {
419 |           var done = finalhandler(req, res)
420 |           res.statusCode = 'oh no'
421 |           done(new Error('oops'))
422 |         })
423 | 
424 |         wrapper(request(server)
425 |           .get('/foo'))
426 |           .expect(500, done)
427 |       })
428 | 
429 |       it('should override with err.status', function (done) {
430 |         var server = createServer(function (req, res) {
431 |           var done = finalhandler(req, res)
432 |           var err = createError('oops', {
433 |             status: 414,
434 |             statusCode: 503
435 |           })
436 |           done(err)
437 |         })
438 | 
439 |         wrapper(request(server)
440 |           .get('/foo'))
441 |           .expect(414, done)
442 |       })
443 | 
444 |       it('should default body to status message in production', function (done) {
445 |         var err = createError('boom!', {
446 |           status: 509
447 |         })
448 |         wrapper(request(createServer(err, {
449 |           env: 'production'
450 |         }))
451 |           .get('/foo'))
452 |           .expect(509, /
Bandwidth Limit Exceeded<\/pre>/, done)
453 |       })
454 |     })
455 | 
456 |     describe('when res.statusCode undefined', function () {
457 |       it('should set to 500', function (done) {
458 |         // http2 does not support non numeric status code
459 |         if (type === 'http2') {
460 |           done()
461 |           return
462 |         }
463 | 
464 |         var server = createServer(function (req, res) {
465 |           var done = finalhandler(req, res)
466 |           res.statusCode = undefined
467 |           done(new Error('oops'))
468 |         })
469 | 
470 |         wrapper(request(server)
471 |           .get('/foo'))
472 |           .expect(500, done)
473 |       })
474 |     })
475 |   })
476 | 
477 |   describe('headers set', function () {
478 |     it('should persist set headers', function (done) {
479 |       var server = createServer(function (req, res) {
480 |         var done = finalhandler(req, res)
481 |         res.setHeader('Server', 'foobar')
482 |         done()
483 |       })
484 | 
485 |       wrapper(request(server)
486 |         .get('/foo'))
487 |         .expect(404)
488 |         .expect('Server', 'foobar')
489 |         .end(done)
490 |     })
491 | 
492 |     it('should override content-type and length', function (done) {
493 |       var server = createServer(function (req, res) {
494 |         var done = finalhandler(req, res)
495 |         res.setHeader('Content-Type', 'image/png')
496 |         res.setHeader('Content-Length', '50')
497 |         done()
498 |       })
499 | 
500 |       wrapper(request(server)
501 |         .get('/foo'))
502 |         .expect(404)
503 |         .expect('Content-Type', 'text/html; charset=utf-8')
504 |         .expect('Content-Length', '142')
505 |         .end(done)
506 |     })
507 | 
508 |     it('should remove other content headers', function (done) {
509 |       var server = createServer(function (req, res) {
510 |         var done = finalhandler(req, res)
511 |         res.setHeader('Content-Encoding', 'gzip')
512 |         res.setHeader('Content-Language', 'jp')
513 |         res.setHeader('Content-Range', 'bytes 0-2/10')
514 |         done()
515 |       })
516 | 
517 |       wrapper(request(server)
518 |         .get('/foo'))
519 |         .expect(404)
520 |         .expect(shouldNotHaveHeader('Content-Encoding'))
521 |         .expect(shouldNotHaveHeader('Content-Language'))
522 |         .expect(shouldNotHaveHeader('Content-Range'))
523 |         .end(done)
524 |     })
525 |   })
526 | 
527 |   describe('request started', function () {
528 |     it('should not respond', function (done) {
529 |       var server = createServer(function (req, res) {
530 |         var done = finalhandler(req, res)
531 |         res.statusCode = 301
532 |         res.write('0')
533 |         process.nextTick(function () {
534 |           done()
535 |           res.end('1')
536 |         })
537 |       })
538 | 
539 |       wrapper(request(server)
540 |         .get('/foo'))
541 |         .expect(301, '01', done)
542 |     })
543 | 
544 |     it('should terminate on error', function (done) {
545 |       var server = createServer(function (req, res) {
546 |         var done = finalhandler(req, res)
547 |         res.statusCode = 301
548 |         res.write('0')
549 |         process.nextTick(function () {
550 |           done(createError('too many requests', {
551 |             status: 429,
552 |             headers: { 'Retry-After': '5' }
553 |           }))
554 |           res.end('1')
555 |         })
556 |       })
557 | 
558 |       wrapper(request(server)
559 |         .get('/foo'))
560 |         .on('request', function onrequest (test) {
561 |           test.req.on('response', function onresponse (res) {
562 |             if (res.listeners('error').length > 0) {
563 |               // forward aborts as errors for supertest
564 |               res.on('aborted', function onabort () {
565 |                 res.emit('error', new Error('aborted'))
566 |               })
567 |             }
568 |           })
569 |         })
570 |         .end(function (err) {
571 |           if (err && err.message !== 'aborted') return done(err)
572 |           assert.strictEqual(this.res.statusCode, 301)
573 |           assert.strictEqual(this.res.text, '0')
574 |           done()
575 |         })
576 |     })
577 |   })
578 | 
579 |   describe('onerror', function () {
580 |     it('should be invoked when error', function (done) {
581 |       var err = new Error('boom!')
582 |       var error
583 | 
584 |       function log (e) {
585 |         error = e
586 |       }
587 | 
588 |       wrapper(request(createServer(err, { onerror: log }))
589 |         .get('/'))
590 |         .end(function () {
591 |           assert.equal(error, err)
592 |           done()
593 |         })
594 |     })
595 |   })
596 | 
597 |   if (parseInt(process.version.split('.')[0].replace(/^v/, ''), 10) > 11) {
598 |     describe('req.socket', function () {
599 |       it('should not throw when socket is null', function (done) {
600 |         wrapper(request(createServer(function (req, res, next) {
601 |           res.statusCode = 200
602 |           res.end('ok')
603 |           process.nextTick(function () {
604 |             req.socket = null
605 |             next(new Error())
606 |           })
607 |         }))
608 |           .get('/'))
609 |           .expect(200)
610 |           .end(function (err) {
611 |             done(err)
612 |           })
613 |       })
614 |     })
615 |   }
616 | 
617 |   describe('no deprecation warnings', function () {
618 |     it('should respond 404 on no error', function (done) {
619 |       var warned = false
620 | 
621 |       process.once('warning', function (warning) {
622 |         if (/The http2 module is an experimental API/.test(warning)) return
623 |         assert.fail(warning)
624 |       })
625 | 
626 |       wrapper(request(createServer())
627 |         .head('/foo'))
628 |         .expect(404)
629 |         .end(function (err) {
630 |           assert.strictEqual(warned, false)
631 |           done(err)
632 |         })
633 |     })
634 | 
635 |     it('should respond 500 on error', function (done) {
636 |       var warned = false
637 | 
638 |       process.once('warning', function (warning) {
639 |         if (/The http2 module is an experimental API/.test(warning)) return
640 |         assert.fail(warning)
641 |       })
642 | 
643 |       var err = createError()
644 | 
645 |       wrapper(request(createServer(function (req, res) {
646 |         var done = finalhandler(req, res)
647 | 
648 |         if (typeof err === 'function') {
649 |           err(req, res, done)
650 |           return
651 |         }
652 | 
653 |         done(err)
654 |       }))
655 |         .head('/foo'))
656 |         .expect(500)
657 |         .end(function (err, res) {
658 |           assert.strictEqual(warned, false)
659 |           done(err)
660 |         })
661 |     })
662 |   })
663 | }
664 | 
665 | var servers = [
666 |   ['http', createHTTPServer],
667 |   ['http2', createHTTP2Server]
668 | ]
669 | 
670 | for (var i = 0; i < servers.length; i++) {
671 |   var tests = topDescribe.bind(undefined, servers[i][0], servers[i][1])
672 | 
673 |   describe(servers[i][0], tests)
674 | }
675 | 


--------------------------------------------------------------------------------