├── .editorconfig ├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ └── scorecard.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── layer.js └── route.js ├── package.json ├── scripts └── version-history.js └── test ├── auto-head.js ├── auto-options.js ├── fqdn-url.js ├── param.js ├── req.params.js ├── route.js ├── router.js └── support └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [{*.js,*.json,*.yml}] 10 | indent_size = 2 11 | indent_style = space 12 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | 29a69f3cf7cb5acbdde0a9151b885d46423b0c72 2 | -------------------------------------------------------------------------------- /.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"] 18 | -------------------------------------------------------------------------------- /.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 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | test: 17 | name: Test - Node.js ${{ matrix.node-version }} 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | node-version: [18, 19, 20, 21, 22, 23] 23 | 24 | steps: 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | 27 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | check-latest: true 31 | 32 | - name: Install Node.js dependencies 33 | run: npm install 34 | 35 | - name: Run tests 36 | run: npm run test-ci 37 | 38 | - name: Lint code 39 | run: npm run lint 40 | 41 | - name: Upload code coverage 42 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 43 | with: 44 | name: coverage-node-${{ matrix.node-version }} 45 | path: ./coverage/lcov.info 46 | retention-days: 1 47 | 48 | coverage: 49 | needs: test 50 | runs-on: ubuntu-latest 51 | permissions: 52 | contents: read 53 | checks: write 54 | steps: 55 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 56 | 57 | - name: Install lcov 58 | shell: bash 59 | run: sudo apt-get -y install lcov 60 | 61 | - name: Collect coverage reports 62 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 63 | with: 64 | path: ./coverage 65 | pattern: coverage-node-* 66 | 67 | - name: Merge coverage reports 68 | shell: bash 69 | run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info 70 | 71 | - name: Upload coverage report 72 | uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 73 | with: 74 | file: ./lcov.info 75 | -------------------------------------------------------------------------------- /.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 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ["javascript"] 39 | # CodeQL supports [ $supported-codeql-languages ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 49 | with: 50 | languages: ${{ matrix.language }} 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@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 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@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 72 | with: 73 | category: "/language:${{matrix.language}}" 74 | -------------------------------------------------------------------------------- /.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@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 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@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 72 | with: 73 | sarif_file: results.sarif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 2.2.0 / 2025-03-26 2 | ================== 3 | 4 | * Remove `setImmediate` support check 5 | * Restore `debug` dependency 6 | 7 | 2.1.0 / 2025-02-10 8 | ================== 9 | 10 | * Updated `engines` field to Node@18 or higher 11 | * Remove `Object.setPrototypeOf` polyfill 12 | * Use `Array.flat` instead of `array-flatten` package 13 | * Replace `methods` dependency with standard library 14 | * deps: parseurl@^1.3.3 15 | * deps: is-promise@^4.0.0 16 | * Replace `utils-merge` dependency with `Object.assign` 17 | * deps: Remove unused dep `after` 18 | 19 | 2.0.0 / 2024-09-09 20 | ================== 21 | 22 | * Drop support for node <18 23 | * deps: path-to-regexp@^8.0.0 24 | - Drop support for partial capture group `router.route('/user(s?)/:user/:op')` but still have optional non-capture `/user{s}/:user/:op` 25 | - `:name?` becomes `{:name}` 26 | - `:name*` becomes `*name`. 27 | - The splat change also changes splat from strings to an array of strings 28 | - Optional splats become `{*name}` 29 | - `:name+` becomes `*name` and thus equivalent to `*name` so I dropped those tests 30 | - Strings as regular expressions are fully removed, need to be converted to native regular expressions 31 | 32 | 2.0.0-beta.2 / 2024-03-20 33 | ========================= 34 | 35 | This incorporates all changes after 1.3.5 up to 1.3.8. 36 | 37 | * Add support for returned, rejected Promises to `router.param` 38 | 39 | 2.0.0-beta.1 / 2020-03-29 40 | ========================= 41 | 42 | This incorporates all changes after 1.3.3 up to 1.3.5. 43 | 44 | * Internalize private `router.process_params` method 45 | * Remove `debug` dependency 46 | * deps: array-flatten@3.0.0 47 | * deps: parseurl@~1.3.3 48 | * deps: path-to-regexp@3.2.0 49 | - Add new `?`, `*`, and `+` parameter modifiers. 50 | - Matching group expressions are only RegExp syntax. 51 | `(*)` is no longer valid and must be written as `(.*)`, for example. 52 | - Named matching groups no longer available by position in `req.params`. 53 | `/:foo(.*)` only captures as `req.params.foo` and not available as 54 | `req.params[0]`. 55 | - Regular expressions can only be used in a matching group. 56 | `/\\d+` is no longer valid and must be written as `/(\\d+)`. 57 | - Matching groups are now literal regular expressions. 58 | `:foo` named captures can no longer be included inside a capture group. 59 | - Special `*` path segment behavior removed. 60 | `/foo/*/bar` will match a literal `*` as the middle segment. 61 | * deps: setprototypeof@1.2.0 62 | 63 | 2.0.0-alpha.1 / 2018-07-27 64 | ========================== 65 | 66 | * Add basic support for returned, rejected Promises 67 | - Rejected Promises from middleware functions `next(error)` 68 | * Drop support for Node.js below 0.10 69 | * deps: debug@3.1.0 70 | - Add `DEBUG_HIDE_DATE` environment variable 71 | - Change timer to per-namespace instead of global 72 | - Change non-TTY date format 73 | - Remove `DEBUG_FD` environment variable support 74 | - Support 256 namespace colors 75 | 76 | 1.3.8 / 2023-02-24 77 | ================== 78 | 79 | * Fix routing requests without method 80 | 81 | 1.3.7 / 2022-04-28 82 | ================== 83 | 84 | * Fix hanging on large stack of sync routes 85 | 86 | 1.3.6 / 2021-11-15 87 | ================== 88 | 89 | * Fix handling very large stacks of sync middleware 90 | * deps: safe-buffer@5.2.1 91 | 92 | 1.3.5 / 2020-03-24 93 | ================== 94 | 95 | * Fix incorrect middleware execution with unanchored `RegExp`s 96 | * perf: use plain object for internal method map 97 | 98 | 1.3.4 / 2020-01-24 99 | ================== 100 | 101 | * deps: array-flatten@3.0.0 102 | * deps: parseurl@~1.3.3 103 | * deps: setprototypeof@1.2.0 104 | 105 | 1.3.3 / 2018-07-06 106 | ================== 107 | 108 | * Fix JSDoc for `Router` constructor 109 | 110 | 1.3.2 / 2017-09-24 111 | ================== 112 | 113 | * deps: debug@2.6.9 114 | * deps: parseurl@~1.3.2 115 | - perf: reduce overhead for full URLs 116 | - perf: unroll the "fast-path" `RegExp` 117 | * deps: setprototypeof@1.1.0 118 | * deps: utils-merge@1.0.1 119 | 120 | 1.3.1 / 2017-05-19 121 | ================== 122 | 123 | * deps: debug@2.6.8 124 | - Fix `DEBUG_MAX_ARRAY_LENGTH` 125 | - deps: ms@2.0.0 126 | 127 | 1.3.0 / 2017-02-25 128 | ================== 129 | 130 | * Add `next("router")` to exit from router 131 | * Fix case where `router.use` skipped requests routes did not 132 | * Use `%o` in path debug to tell types apart 133 | * deps: setprototypeof@1.0.3 134 | * perf: add fast match path for `*` route 135 | 136 | 1.2.0 / 2017-02-17 137 | ================== 138 | 139 | * Skip routing when `req.url` is not set 140 | * deps: debug@2.6.1 141 | - Allow colors in workers 142 | - Deprecated `DEBUG_FD` environment variable set to `3` or higher 143 | - Fix error when running under React Native 144 | - Use same color for same namespace 145 | - deps: ms@0.7.2 146 | 147 | 1.1.5 / 2017-01-28 148 | ================== 149 | 150 | * deps: array-flatten@2.1.1 151 | * deps: setprototypeof@1.0.2 152 | - Fix using fallback even when native method exists 153 | 154 | 1.1.4 / 2016-01-21 155 | ================== 156 | 157 | * deps: array-flatten@2.0.0 158 | * deps: methods@~1.1.2 159 | - perf: enable strict mode 160 | * deps: parseurl@~1.3.1 161 | - perf: enable strict mode 162 | 163 | 1.1.3 / 2015-08-02 164 | ================== 165 | 166 | * Fix infinite loop condition using `mergeParams: true` 167 | * Fix inner numeric indices incorrectly altering parent `req.params` 168 | * deps: array-flatten@1.1.1 169 | - perf: enable strict mode 170 | * deps: path-to-regexp@0.1.7 171 | - Fix regression with escaped round brackets and matching groups 172 | 173 | 1.1.2 / 2015-07-06 174 | ================== 175 | 176 | * Fix hiding platform issues with `decodeURIComponent` 177 | - Only `URIError`s are a 400 178 | * Fix using `*` before params in routes 179 | * Fix using capture groups before params in routes 180 | * deps: path-to-regexp@0.1.6 181 | * perf: enable strict mode 182 | * perf: remove argument reassignments in routing 183 | * perf: skip attempting to decode zero length string 184 | * perf: use plain for loops 185 | 186 | 1.1.1 / 2015-05-25 187 | ================== 188 | 189 | * Fix issue where `next('route')` in `router.param` would incorrectly skip values 190 | * deps: array-flatten@1.1.0 191 | * deps: debug@~2.2.0 192 | - deps: ms@0.7.1 193 | 194 | 1.1.0 / 2015-04-22 195 | ================== 196 | 197 | * Use `setprototypeof` instead of `__proto__` 198 | * deps: debug@~2.1.3 199 | - Fix high intensity foreground color for bold 200 | - deps: ms@0.7.0 201 | 202 | 1.0.0 / 2015-01-13 203 | ================== 204 | 205 | * Fix crash from error within `OPTIONS` response handler 206 | * deps: array-flatten@1.0.2 207 | - Remove redundant code path 208 | 209 | 1.0.0-beta.3 / 2015-01-11 210 | ========================= 211 | 212 | * Fix duplicate methods appearing in OPTIONS responses 213 | * Fix OPTIONS responses to include the HEAD method properly 214 | * Remove support for leading colon in `router.param(name, fn)` 215 | * Use `array-flatten` for flattening arrays 216 | * deps: debug@~2.1.1 217 | * deps: methods@~1.1.1 218 | 219 | 1.0.0-beta.2 / 2014-11-19 220 | ========================= 221 | 222 | * Match routes iteratively to prevent stack overflows 223 | 224 | 1.0.0-beta.1 / 2014-11-16 225 | ========================= 226 | 227 | * Initial release ported from Express 4.x 228 | - Altered to work without Express 229 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Roman Shtylman 4 | Copyright (c) 2014-2022 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 | # router 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![NPM Downloads][downloads-image]][downloads-url] 5 | [![Node.js Version][node-version-image]][node-version-url] 6 | [![Build Status][ci-image]][ci-url] 7 | [![Test Coverage][coveralls-image]][coveralls-url] 8 | 9 | Simple middleware-style router 10 | 11 | ## Installation 12 | 13 | This is a [Node.js](https://nodejs.org/en/) module available through the 14 | [npm registry](https://www.npmjs.com/). Installation is done using the 15 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 16 | 17 | ```bash 18 | $ npm install router 19 | ``` 20 | 21 | ## API 22 | 23 | ```js 24 | var finalhandler = require('finalhandler') 25 | var http = require('http') 26 | var Router = require('router') 27 | 28 | var router = Router() 29 | router.get('/', function (req, res) { 30 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 31 | res.end('Hello World!') 32 | }) 33 | 34 | var server = http.createServer(function (req, res) { 35 | router(req, res, finalhandler(req, res)) 36 | }) 37 | 38 | server.listen(3000) 39 | ``` 40 | 41 | This module is currently an extracted version from the Express project, 42 | but with the main change being it can be used with a plain `http.createServer` 43 | object or other web frameworks by removing Express-specific API calls. 44 | 45 | ## Router(options) 46 | 47 | Options 48 | 49 | - `strict` - When `false` trailing slashes are optional (default: `false`) 50 | - `caseSensitive` - When `true` the routing will be case sensitive. (default: `false`) 51 | - `mergeParams` - When `true` any `req.params` passed to the router will be 52 | merged into the router's `req.params`. (default: `false`) ([example](#example-using-mergeparams)) 53 | 54 | Returns a function with the signature `router(req, res, callback)` where 55 | `callback([err])` must be provided to handle errors and fall-through from 56 | not handling requests. 57 | 58 | ### router.use([path], ...middleware) 59 | 60 | Use the given [middleware function](#middleware) for all http methods on the 61 | given `path`, defaulting to the root path. 62 | 63 | `router` does not automatically see `use` as a handler. As such, it will not 64 | consider it one for handling `OPTIONS` requests. 65 | 66 | * Note: If a `path` is specified, that `path` is stripped from the start of 67 | `req.url`. 68 | 69 | 70 | 71 | ```js 72 | router.use(function (req, res, next) { 73 | // do your things 74 | 75 | // continue to the next middleware 76 | // the request will stall if this is not called 77 | next() 78 | 79 | // note: you should NOT call `next` if you have begun writing to the response 80 | }) 81 | ``` 82 | 83 | [Middleware](#middleware) can themselves use `next('router')` at any time to 84 | exit the current router instance completely, invoking the top-level callback. 85 | 86 | ### router\[method](path, ...[middleware], handler) 87 | 88 | The [http methods](https://github.com/jshttp/methods/blob/master/index.js) provide 89 | the routing functionality in `router`. 90 | 91 | Method middleware and handlers follow usual [middleware](#middleware) behavior, 92 | except they will only be called when the method and path match the request. 93 | 94 | 95 | 96 | ```js 97 | // handle a `GET` request 98 | router.get('/', function (req, res) { 99 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 100 | res.end('Hello World!') 101 | }) 102 | ``` 103 | 104 | [Middleware](#middleware) given before the handler have one additional trick, 105 | they may invoke `next('route')`. Calling `next('route')` bypasses the remaining 106 | middleware and the handler mounted for this route, passing the request to the 107 | next route suitable for handling this request. 108 | 109 | Route handlers and middleware can themselves use `next('router')` at any time 110 | to exit the current router instance completely, invoking the top-level callback. 111 | 112 | ### router.param(name, param_middleware) 113 | 114 | Maps the specified path parameter `name` to a specialized param-capturing middleware. 115 | 116 | This function positions the middleware in the same stack as `.use`. 117 | 118 | The function can optionally return a `Promise` object. If a `Promise` object 119 | is returned from the function, the router will attach an `onRejected` callback 120 | using `.then`. If the promise is rejected, `next` will be called with the 121 | rejected value, or an error if the value is falsy. 122 | 123 | Parameter mapping is used to provide pre-conditions to routes 124 | which use normalized placeholders. For example a _:user_id_ parameter 125 | could automatically load a user's information from the database without 126 | any additional code: 127 | 128 | 129 | 130 | ```js 131 | router.param('user_id', function (req, res, next, id) { 132 | User.find(id, function (err, user) { 133 | if (err) { 134 | return next(err) 135 | } else if (!user) { 136 | return next(new Error('failed to load user')) 137 | } 138 | req.user = user 139 | 140 | // continue processing the request 141 | next() 142 | }) 143 | }) 144 | ``` 145 | 146 | ### router.route(path) 147 | 148 | Creates an instance of a single `Route` for the given `path`. 149 | (See `Router.Route` below) 150 | 151 | Routes can be used to handle http `methods` with their own, optional middleware. 152 | 153 | Using `router.route(path)` is a recommended approach to avoiding duplicate 154 | route naming and thus typo errors. 155 | 156 | 157 | 158 | ```js 159 | var api = router.route('/api/') 160 | ``` 161 | 162 | ## Router.Route(path) 163 | 164 | Represents a single route as an instance that can be used to handle http 165 | `methods` with it's own, optional middleware. 166 | 167 | ### route\[method](handler) 168 | 169 | These are functions which you can directly call on a route to register a new 170 | `handler` for the `method` on the route. 171 | 172 | 173 | 174 | ```js 175 | // handle a `GET` request 176 | var status = router.route('/status') 177 | 178 | status.get(function (req, res) { 179 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 180 | res.end('All Systems Green!') 181 | }) 182 | ``` 183 | 184 | ### route.all(handler) 185 | 186 | Adds a handler for all HTTP methods to this route. 187 | 188 | The handler can behave like middleware and call `next` to continue processing 189 | rather than responding. 190 | 191 | 192 | 193 | ```js 194 | router.route('/') 195 | .all(function (req, res, next) { 196 | next() 197 | }) 198 | .all(checkSomething) 199 | .get(function (req, res) { 200 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 201 | res.end('Hello World!') 202 | }) 203 | ``` 204 | 205 | ## Middleware 206 | 207 | Middleware (and method handlers) are functions that follow specific function 208 | parameters and have defined behavior when used with `router`. The most common 209 | format is with three parameters - "req", "res" and "next". 210 | 211 | - `req` - This is a [HTTP incoming message](https://nodejs.org/api/http.html#http_http_incomingmessage) instance. 212 | - `res` - This is a [HTTP server response](https://nodejs.org/api/http.html#http_class_http_serverresponse) instance. 213 | - `next` - Calling this function that tells `router` to proceed to the next matching middleware or method handler. It accepts an error as the first argument. 214 | 215 | The function can optionally return a `Promise` object. If a `Promise` object 216 | is returned from the function, the router will attach an `onRejected` callback 217 | using `.then`. If the promise is rejected, `next` will be called with the 218 | rejected value, or an error if the value is falsy. 219 | 220 | Middleware and method handlers can also be defined with four arguments. When 221 | the function has four parameters defined, the first argument is an error and 222 | subsequent arguments remain, becoming - "err", "req", "res", "next". These 223 | functions are "error handling middleware", and can be used for handling 224 | errors that occurred in previous handlers (E.g. from calling `next(err)`). 225 | This is most used when you want to define arbitrary rendering of errors. 226 | 227 | 228 | 229 | ```js 230 | router.get('/error_route', function (req, res, next) { 231 | return next(new Error('Bad Request')) 232 | }) 233 | 234 | router.use(function (err, req, res, next) { 235 | res.end(err.message) //= > "Bad Request" 236 | }) 237 | ``` 238 | 239 | Error handling middleware will **only** be invoked when an error was given. As 240 | long as the error is in the pipeline, normal middleware and handlers will be 241 | bypassed - only error handling middleware will be invoked with an error. 242 | 243 | ## Examples 244 | 245 | ```js 246 | // import our modules 247 | var http = require('http') 248 | var Router = require('router') 249 | var finalhandler = require('finalhandler') 250 | var compression = require('compression') 251 | var bodyParser = require('body-parser') 252 | 253 | // store our message to display 254 | var message = 'Hello World!' 255 | 256 | // initialize the router & server and add a final callback. 257 | var router = Router() 258 | var server = http.createServer(function onRequest (req, res) { 259 | router(req, res, finalhandler(req, res)) 260 | }) 261 | 262 | // use some middleware and compress all outgoing responses 263 | router.use(compression()) 264 | 265 | // handle `GET` requests to `/message` 266 | router.get('/message', function (req, res) { 267 | res.statusCode = 200 268 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 269 | res.end(message + '\n') 270 | }) 271 | 272 | // create and mount a new router for our API 273 | var api = Router() 274 | router.use('/api/', api) 275 | 276 | // add a body parsing middleware to our API 277 | api.use(bodyParser.json()) 278 | 279 | // handle `PATCH` requests to `/api/set-message` 280 | api.patch('/set-message', function (req, res) { 281 | if (req.body.value) { 282 | message = req.body.value 283 | 284 | res.statusCode = 200 285 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 286 | res.end(message + '\n') 287 | } else { 288 | res.statusCode = 400 289 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 290 | res.end('Invalid API Syntax\n') 291 | } 292 | }) 293 | 294 | // make our http server listen to connections 295 | server.listen(8080) 296 | ``` 297 | 298 | You can get the message by running this command in your terminal, 299 | or navigating to `127.0.0.1:8080` in a web browser. 300 | ```bash 301 | curl http://127.0.0.1:8080 302 | ``` 303 | 304 | You can set the message by sending it a `PATCH` request via this command: 305 | ```bash 306 | curl http://127.0.0.1:8080/api/set-message -X PATCH -H "Content-Type: application/json" -d '{"value":"Cats!"}' 307 | ``` 308 | 309 | ### Example using mergeParams 310 | 311 | ```js 312 | var http = require('http') 313 | var Router = require('router') 314 | var finalhandler = require('finalhandler') 315 | 316 | // this example is about the mergeParams option 317 | var opts = { mergeParams: true } 318 | 319 | // make a router with out special options 320 | var router = Router(opts) 321 | var server = http.createServer(function onRequest (req, res) { 322 | // set something to be passed into the router 323 | req.params = { type: 'kitten' } 324 | 325 | router(req, res, finalhandler(req, res)) 326 | }) 327 | 328 | router.get('/', function (req, res) { 329 | res.statusCode = 200 330 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 331 | 332 | // with respond with the the params that were passed in 333 | res.end(req.params.type + '\n') 334 | }) 335 | 336 | // make another router with our options 337 | var handler = Router(opts) 338 | 339 | // mount our new router to a route that accepts a param 340 | router.use('/:path', handler) 341 | 342 | handler.get('/', function (req, res) { 343 | res.statusCode = 200 344 | res.setHeader('Content-Type', 'text/plain; charset=utf-8') 345 | 346 | // will respond with the param of the router's parent route 347 | res.end(req.params.path + '\n') 348 | }) 349 | 350 | // make our http server listen to connections 351 | server.listen(8080) 352 | ``` 353 | 354 | Now you can get the type, or what path you are requesting: 355 | ```bash 356 | curl http://127.0.0.1:8080 357 | > kitten 358 | curl http://127.0.0.1:8080/such_path 359 | > such_path 360 | ``` 361 | 362 | ### Example of advanced `.route()` usage 363 | 364 | This example shows how to implement routes where there is a custom 365 | handler to execute when the path matched, but no methods matched. 366 | Without any special handling, this would be treated as just a 367 | generic non-match by `router` (which typically results in a 404), 368 | but with a custom handler, a `405 Method Not Allowed` can be sent. 369 | 370 | ```js 371 | var http = require('http') 372 | var finalhandler = require('finalhandler') 373 | var Router = require('router') 374 | 375 | // create the router and server 376 | var router = new Router() 377 | var server = http.createServer(function onRequest (req, res) { 378 | router(req, res, finalhandler(req, res)) 379 | }) 380 | 381 | // register a route and add all methods 382 | router.route('/pet/:id') 383 | .get(function (req, res) { 384 | // this is GET /pet/:id 385 | res.setHeader('Content-Type', 'application/json') 386 | res.end(JSON.stringify({ name: 'tobi' })) 387 | }) 388 | .delete(function (req, res) { 389 | // this is DELETE /pet/:id 390 | res.end() 391 | }) 392 | .all(function (req, res) { 393 | // this is called for all other methods not 394 | // defined above for /pet/:id 395 | res.statusCode = 405 396 | res.end() 397 | }) 398 | 399 | // make our http server listen to connections 400 | server.listen(8080) 401 | ``` 402 | 403 | ## License 404 | 405 | [MIT](LICENSE) 406 | 407 | [ci-image]: https://badgen.net/github/checks/pillarjs/router/master?label=ci 408 | [ci-url]: https://github.com/pillarjs/router/actions/workflows/ci.yml 409 | [npm-image]: https://img.shields.io/npm/v/router.svg 410 | [npm-url]: https://npmjs.org/package/router 411 | [node-version-image]: https://img.shields.io/node/v/router.svg 412 | [node-version-url]: http://nodejs.org/download/ 413 | [coveralls-image]: https://img.shields.io/coveralls/pillarjs/router/master.svg 414 | [coveralls-url]: https://coveralls.io/r/pillarjs/router?branch=master 415 | [downloads-image]: https://img.shields.io/npm/dm/router.svg 416 | [downloads-url]: https://npmjs.org/package/router 417 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * router 3 | * Copyright(c) 2013 Roman Shtylman 4 | * Copyright(c) 2014-2022 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module dependencies. 12 | * @private 13 | */ 14 | 15 | const isPromise = require('is-promise') 16 | const Layer = require('./lib/layer') 17 | const { METHODS } = require('node:http') 18 | const parseUrl = require('parseurl') 19 | const Route = require('./lib/route') 20 | const debug = require('debug')('router') 21 | const deprecate = require('depd')('router') 22 | 23 | /** 24 | * Module variables. 25 | * @private 26 | */ 27 | 28 | const slice = Array.prototype.slice 29 | const flatten = Array.prototype.flat 30 | const methods = METHODS.map((method) => method.toLowerCase()) 31 | 32 | /** 33 | * Expose `Router`. 34 | */ 35 | 36 | module.exports = Router 37 | 38 | /** 39 | * Expose `Route`. 40 | */ 41 | 42 | module.exports.Route = Route 43 | 44 | /** 45 | * Initialize a new `Router` with the given `options`. 46 | * 47 | * @param {object} [options] 48 | * @return {Router} which is a callable function 49 | * @public 50 | */ 51 | 52 | function Router (options) { 53 | if (!(this instanceof Router)) { 54 | return new Router(options) 55 | } 56 | 57 | const opts = options || {} 58 | 59 | function router (req, res, next) { 60 | router.handle(req, res, next) 61 | } 62 | 63 | // inherit from the correct prototype 64 | Object.setPrototypeOf(router, this) 65 | 66 | router.caseSensitive = opts.caseSensitive 67 | router.mergeParams = opts.mergeParams 68 | router.params = {} 69 | router.strict = opts.strict 70 | router.stack = [] 71 | 72 | return router 73 | } 74 | 75 | /** 76 | * Router prototype inherits from a Function. 77 | */ 78 | 79 | /* istanbul ignore next */ 80 | Router.prototype = function () {} 81 | 82 | /** 83 | * Map the given param placeholder `name`(s) to the given callback. 84 | * 85 | * Parameter mapping is used to provide pre-conditions to routes 86 | * which use normalized placeholders. For example a _:user_id_ parameter 87 | * could automatically load a user's information from the database without 88 | * any additional code. 89 | * 90 | * The callback uses the same signature as middleware, the only difference 91 | * being that the value of the placeholder is passed, in this case the _id_ 92 | * of the user. Once the `next()` function is invoked, just like middleware 93 | * it will continue on to execute the route, or subsequent parameter functions. 94 | * 95 | * Just like in middleware, you must either respond to the request or call next 96 | * to avoid stalling the request. 97 | * 98 | * router.param('user_id', function(req, res, next, id){ 99 | * User.find(id, function(err, user){ 100 | * if (err) { 101 | * return next(err) 102 | * } else if (!user) { 103 | * return next(new Error('failed to load user')) 104 | * } 105 | * req.user = user 106 | * next() 107 | * }) 108 | * }) 109 | * 110 | * @param {string} name 111 | * @param {function} fn 112 | * @public 113 | */ 114 | 115 | Router.prototype.param = function param (name, fn) { 116 | if (!name) { 117 | throw new TypeError('argument name is required') 118 | } 119 | 120 | if (typeof name !== 'string') { 121 | throw new TypeError('argument name must be a string') 122 | } 123 | 124 | if (!fn) { 125 | throw new TypeError('argument fn is required') 126 | } 127 | 128 | if (typeof fn !== 'function') { 129 | throw new TypeError('argument fn must be a function') 130 | } 131 | 132 | let params = this.params[name] 133 | 134 | if (!params) { 135 | params = this.params[name] = [] 136 | } 137 | 138 | params.push(fn) 139 | 140 | return this 141 | } 142 | 143 | /** 144 | * Dispatch a req, res into the router. 145 | * 146 | * @private 147 | */ 148 | 149 | Router.prototype.handle = function handle (req, res, callback) { 150 | if (!callback) { 151 | throw new TypeError('argument callback is required') 152 | } 153 | 154 | debug('dispatching %s %s', req.method, req.url) 155 | 156 | let idx = 0 157 | let methods 158 | const protohost = getProtohost(req.url) || '' 159 | let removed = '' 160 | const self = this 161 | let slashAdded = false 162 | let sync = 0 163 | const paramcalled = {} 164 | 165 | // middleware and routes 166 | const stack = this.stack 167 | 168 | // manage inter-router variables 169 | const parentParams = req.params 170 | const parentUrl = req.baseUrl || '' 171 | let done = restore(callback, req, 'baseUrl', 'next', 'params') 172 | 173 | // setup next layer 174 | req.next = next 175 | 176 | // for options requests, respond with a default if nothing else responds 177 | if (req.method === 'OPTIONS') { 178 | methods = [] 179 | done = wrap(done, generateOptionsResponder(res, methods)) 180 | } 181 | 182 | // setup basic req values 183 | req.baseUrl = parentUrl 184 | req.originalUrl = req.originalUrl || req.url 185 | 186 | next() 187 | 188 | function next (err) { 189 | let layerError = err === 'route' 190 | ? null 191 | : err 192 | 193 | // remove added slash 194 | if (slashAdded) { 195 | req.url = req.url.slice(1) 196 | slashAdded = false 197 | } 198 | 199 | // restore altered req.url 200 | if (removed.length !== 0) { 201 | req.baseUrl = parentUrl 202 | req.url = protohost + removed + req.url.slice(protohost.length) 203 | removed = '' 204 | } 205 | 206 | // signal to exit router 207 | if (layerError === 'router') { 208 | setImmediate(done, null) 209 | return 210 | } 211 | 212 | // no more matching layers 213 | if (idx >= stack.length) { 214 | setImmediate(done, layerError) 215 | return 216 | } 217 | 218 | // max sync stack 219 | if (++sync > 100) { 220 | return setImmediate(next, err) 221 | } 222 | 223 | // get pathname of request 224 | const path = getPathname(req) 225 | 226 | if (path == null) { 227 | return done(layerError) 228 | } 229 | 230 | // find next matching layer 231 | let layer 232 | let match 233 | let route 234 | 235 | while (match !== true && idx < stack.length) { 236 | layer = stack[idx++] 237 | match = matchLayer(layer, path) 238 | route = layer.route 239 | 240 | if (typeof match !== 'boolean') { 241 | // hold on to layerError 242 | layerError = layerError || match 243 | } 244 | 245 | if (match !== true) { 246 | continue 247 | } 248 | 249 | if (!route) { 250 | // process non-route handlers normally 251 | continue 252 | } 253 | 254 | if (layerError) { 255 | // routes do not match with a pending error 256 | match = false 257 | continue 258 | } 259 | 260 | const method = req.method 261 | const hasMethod = route._handlesMethod(method) 262 | 263 | // build up automatic options response 264 | if (!hasMethod && method === 'OPTIONS' && methods) { 265 | methods.push.apply(methods, route._methods()) 266 | } 267 | 268 | // don't even bother matching route 269 | if (!hasMethod && method !== 'HEAD') { 270 | match = false 271 | } 272 | } 273 | 274 | // no match 275 | if (match !== true) { 276 | return done(layerError) 277 | } 278 | 279 | // store route for dispatch on change 280 | if (route) { 281 | req.route = route 282 | } 283 | 284 | // Capture one-time layer values 285 | req.params = self.mergeParams 286 | ? mergeParams(layer.params, parentParams) 287 | : layer.params 288 | const layerPath = layer.path 289 | 290 | // this should be done for the layer 291 | processParams(self.params, layer, paramcalled, req, res, function (err) { 292 | if (err) { 293 | next(layerError || err) 294 | } else if (route) { 295 | layer.handleRequest(req, res, next) 296 | } else { 297 | trimPrefix(layer, layerError, layerPath, path) 298 | } 299 | 300 | sync = 0 301 | }) 302 | } 303 | 304 | function trimPrefix (layer, layerError, layerPath, path) { 305 | if (layerPath.length !== 0) { 306 | // Validate path is a prefix match 307 | if (layerPath !== path.substring(0, layerPath.length)) { 308 | next(layerError) 309 | return 310 | } 311 | 312 | // Validate path breaks on a path separator 313 | const c = path[layerPath.length] 314 | if (c && c !== '/') { 315 | next(layerError) 316 | return 317 | } 318 | 319 | // Trim off the part of the url that matches the route 320 | // middleware (.use stuff) needs to have the path stripped 321 | debug('trim prefix (%s) from url %s', layerPath, req.url) 322 | removed = layerPath 323 | req.url = protohost + req.url.slice(protohost.length + removed.length) 324 | 325 | // Ensure leading slash 326 | if (!protohost && req.url[0] !== '/') { 327 | req.url = '/' + req.url 328 | slashAdded = true 329 | } 330 | 331 | // Setup base URL (no trailing slash) 332 | req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' 333 | ? removed.substring(0, removed.length - 1) 334 | : removed) 335 | } 336 | 337 | debug('%s %s : %s', layer.name, layerPath, req.originalUrl) 338 | 339 | if (layerError) { 340 | layer.handleError(layerError, req, res, next) 341 | } else { 342 | layer.handleRequest(req, res, next) 343 | } 344 | } 345 | } 346 | 347 | /** 348 | * Use the given middleware function, with optional path, defaulting to "/". 349 | * 350 | * Use (like `.all`) will run for any http METHOD, but it will not add 351 | * handlers for those methods so OPTIONS requests will not consider `.use` 352 | * functions even if they could respond. 353 | * 354 | * The other difference is that _route_ path is stripped and not visible 355 | * to the handler function. The main effect of this feature is that mounted 356 | * handlers can operate without any code changes regardless of the "prefix" 357 | * pathname. 358 | * 359 | * @public 360 | */ 361 | 362 | Router.prototype.use = function use (handler) { 363 | let offset = 0 364 | let path = '/' 365 | 366 | // default path to '/' 367 | // disambiguate router.use([handler]) 368 | if (typeof handler !== 'function') { 369 | let arg = handler 370 | 371 | while (Array.isArray(arg) && arg.length !== 0) { 372 | arg = arg[0] 373 | } 374 | 375 | // first arg is the path 376 | if (typeof arg !== 'function') { 377 | offset = 1 378 | path = handler 379 | } 380 | } 381 | 382 | const callbacks = flatten.call(slice.call(arguments, offset), Infinity) 383 | 384 | if (callbacks.length === 0) { 385 | throw new TypeError('argument handler is required') 386 | } 387 | 388 | for (let i = 0; i < callbacks.length; i++) { 389 | const fn = callbacks[i] 390 | 391 | if (typeof fn !== 'function') { 392 | throw new TypeError('argument handler must be a function') 393 | } 394 | 395 | // add the middleware 396 | debug('use %o %s', path, fn.name || '') 397 | 398 | const layer = new Layer(path, { 399 | sensitive: this.caseSensitive, 400 | strict: false, 401 | end: false 402 | }, fn) 403 | 404 | layer.route = undefined 405 | 406 | this.stack.push(layer) 407 | } 408 | 409 | return this 410 | } 411 | 412 | /** 413 | * Create a new Route for the given path. 414 | * 415 | * Each route contains a separate middleware stack and VERB handlers. 416 | * 417 | * See the Route api documentation for details on adding handlers 418 | * and middleware to routes. 419 | * 420 | * @param {string} path 421 | * @return {Route} 422 | * @public 423 | */ 424 | 425 | Router.prototype.route = function route (path) { 426 | const route = new Route(path) 427 | 428 | const layer = new Layer(path, { 429 | sensitive: this.caseSensitive, 430 | strict: this.strict, 431 | end: true 432 | }, handle) 433 | 434 | function handle (req, res, next) { 435 | route.dispatch(req, res, next) 436 | } 437 | 438 | layer.route = route 439 | 440 | this.stack.push(layer) 441 | return route 442 | } 443 | 444 | // create Router#VERB functions 445 | methods.concat('all').forEach(function (method) { 446 | Router.prototype[method] = function (path) { 447 | const route = this.route(path) 448 | route[method].apply(route, slice.call(arguments, 1)) 449 | return this 450 | } 451 | }) 452 | 453 | /** 454 | * Generate a callback that will make an OPTIONS response. 455 | * 456 | * @param {OutgoingMessage} res 457 | * @param {array} methods 458 | * @private 459 | */ 460 | 461 | function generateOptionsResponder (res, methods) { 462 | return function onDone (fn, err) { 463 | if (err || methods.length === 0) { 464 | return fn(err) 465 | } 466 | 467 | trySendOptionsResponse(res, methods, fn) 468 | } 469 | } 470 | 471 | /** 472 | * Get pathname of request. 473 | * 474 | * @param {IncomingMessage} req 475 | * @private 476 | */ 477 | 478 | function getPathname (req) { 479 | try { 480 | return parseUrl(req).pathname 481 | } catch (err) { 482 | return undefined 483 | } 484 | } 485 | 486 | /** 487 | * Get get protocol + host for a URL. 488 | * 489 | * @param {string} url 490 | * @private 491 | */ 492 | 493 | function getProtohost (url) { 494 | if (typeof url !== 'string' || url.length === 0 || url[0] === '/') { 495 | return undefined 496 | } 497 | 498 | const searchIndex = url.indexOf('?') 499 | const pathLength = searchIndex !== -1 500 | ? searchIndex 501 | : url.length 502 | const fqdnIndex = url.substring(0, pathLength).indexOf('://') 503 | 504 | return fqdnIndex !== -1 505 | ? url.substring(0, url.indexOf('/', 3 + fqdnIndex)) 506 | : undefined 507 | } 508 | 509 | /** 510 | * Match path to a layer. 511 | * 512 | * @param {Layer} layer 513 | * @param {string} path 514 | * @private 515 | */ 516 | 517 | function matchLayer (layer, path) { 518 | try { 519 | return layer.match(path) 520 | } catch (err) { 521 | return err 522 | } 523 | } 524 | 525 | /** 526 | * Merge params with parent params 527 | * 528 | * @private 529 | */ 530 | 531 | function mergeParams (params, parent) { 532 | if (typeof parent !== 'object' || !parent) { 533 | return params 534 | } 535 | 536 | // make copy of parent for base 537 | const obj = Object.assign({}, parent) 538 | 539 | // simple non-numeric merging 540 | if (!(0 in params) || !(0 in parent)) { 541 | return Object.assign(obj, params) 542 | } 543 | 544 | let i = 0 545 | let o = 0 546 | 547 | // determine numeric gap in params 548 | while (i in params) { 549 | i++ 550 | } 551 | 552 | // determine numeric gap in parent 553 | while (o in parent) { 554 | o++ 555 | } 556 | 557 | // offset numeric indices in params before merge 558 | for (i--; i >= 0; i--) { 559 | params[i + o] = params[i] 560 | 561 | // create holes for the merge when necessary 562 | if (i < o) { 563 | delete params[i] 564 | } 565 | } 566 | 567 | return Object.assign(obj, params) 568 | } 569 | 570 | /** 571 | * Process any parameters for the layer. 572 | * 573 | * @private 574 | */ 575 | 576 | function processParams (params, layer, called, req, res, done) { 577 | // captured parameters from the layer, keys and values 578 | const keys = layer.keys 579 | 580 | // fast track 581 | if (!keys || keys.length === 0) { 582 | return done() 583 | } 584 | 585 | let i = 0 586 | let paramIndex = 0 587 | let key 588 | let paramVal 589 | let paramCallbacks 590 | let paramCalled 591 | 592 | // process params in order 593 | // param callbacks can be async 594 | function param (err) { 595 | if (err) { 596 | return done(err) 597 | } 598 | 599 | if (i >= keys.length) { 600 | return done() 601 | } 602 | 603 | paramIndex = 0 604 | key = keys[i++] 605 | paramVal = req.params[key] 606 | paramCallbacks = params[key] 607 | paramCalled = called[key] 608 | 609 | if (paramVal === undefined || !paramCallbacks) { 610 | return param() 611 | } 612 | 613 | // param previously called with same value or error occurred 614 | if (paramCalled && (paramCalled.match === paramVal || 615 | (paramCalled.error && paramCalled.error !== 'route'))) { 616 | // restore value 617 | req.params[key] = paramCalled.value 618 | 619 | // next param 620 | return param(paramCalled.error) 621 | } 622 | 623 | called[key] = paramCalled = { 624 | error: null, 625 | match: paramVal, 626 | value: paramVal 627 | } 628 | 629 | paramCallback() 630 | } 631 | 632 | // single param callbacks 633 | function paramCallback (err) { 634 | const fn = paramCallbacks[paramIndex++] 635 | 636 | // store updated value 637 | paramCalled.value = req.params[key] 638 | 639 | if (err) { 640 | // store error 641 | paramCalled.error = err 642 | param(err) 643 | return 644 | } 645 | 646 | if (!fn) return param() 647 | 648 | try { 649 | const ret = fn(req, res, paramCallback, paramVal, key) 650 | if (isPromise(ret)) { 651 | if (!(ret instanceof Promise)) { 652 | deprecate('parameters that are Promise-like are deprecated, use a native Promise instead') 653 | } 654 | 655 | ret.then(null, function (error) { 656 | paramCallback(error || new Error('Rejected promise')) 657 | }) 658 | } 659 | } catch (e) { 660 | paramCallback(e) 661 | } 662 | } 663 | 664 | param() 665 | } 666 | 667 | /** 668 | * Restore obj props after function 669 | * 670 | * @private 671 | */ 672 | 673 | function restore (fn, obj) { 674 | const props = new Array(arguments.length - 2) 675 | const vals = new Array(arguments.length - 2) 676 | 677 | for (let i = 0; i < props.length; i++) { 678 | props[i] = arguments[i + 2] 679 | vals[i] = obj[props[i]] 680 | } 681 | 682 | return function () { 683 | // restore vals 684 | for (let i = 0; i < props.length; i++) { 685 | obj[props[i]] = vals[i] 686 | } 687 | 688 | return fn.apply(this, arguments) 689 | } 690 | } 691 | 692 | /** 693 | * Send an OPTIONS response. 694 | * 695 | * @private 696 | */ 697 | 698 | function sendOptionsResponse (res, methods) { 699 | const options = Object.create(null) 700 | 701 | // build unique method map 702 | for (let i = 0; i < methods.length; i++) { 703 | options[methods[i]] = true 704 | } 705 | 706 | // construct the allow list 707 | const allow = Object.keys(options).sort().join(', ') 708 | 709 | // send response 710 | res.setHeader('Allow', allow) 711 | res.setHeader('Content-Length', Buffer.byteLength(allow)) 712 | res.setHeader('Content-Type', 'text/plain') 713 | res.setHeader('X-Content-Type-Options', 'nosniff') 714 | res.end(allow) 715 | } 716 | 717 | /** 718 | * Try to send an OPTIONS response. 719 | * 720 | * @private 721 | */ 722 | 723 | function trySendOptionsResponse (res, methods, next) { 724 | try { 725 | sendOptionsResponse(res, methods) 726 | } catch (err) { 727 | next(err) 728 | } 729 | } 730 | 731 | /** 732 | * Wrap a function 733 | * 734 | * @private 735 | */ 736 | 737 | function wrap (old, fn) { 738 | return function proxy () { 739 | const args = new Array(arguments.length + 1) 740 | 741 | args[0] = old 742 | for (let i = 0, len = arguments.length; i < len; i++) { 743 | args[i + 1] = arguments[i] 744 | } 745 | 746 | fn.apply(this, args) 747 | } 748 | } 749 | -------------------------------------------------------------------------------- /lib/layer.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * router 3 | * Copyright(c) 2013 Roman Shtylman 4 | * Copyright(c) 2014-2022 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module dependencies. 12 | * @private 13 | */ 14 | 15 | const isPromise = require('is-promise') 16 | const pathRegexp = require('path-to-regexp') 17 | const debug = require('debug')('router:layer') 18 | const deprecate = require('depd')('router') 19 | 20 | /** 21 | * Module variables. 22 | * @private 23 | */ 24 | 25 | const TRAILING_SLASH_REGEXP = /\/+$/ 26 | const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g 27 | 28 | /** 29 | * Expose `Layer`. 30 | */ 31 | 32 | module.exports = Layer 33 | 34 | function Layer (path, options, fn) { 35 | if (!(this instanceof Layer)) { 36 | return new Layer(path, options, fn) 37 | } 38 | 39 | debug('new %o', path) 40 | const opts = options || {} 41 | 42 | this.handle = fn 43 | this.keys = [] 44 | this.name = fn.name || '' 45 | this.params = undefined 46 | this.path = undefined 47 | this.slash = path === '/' && opts.end === false 48 | 49 | function matcher (_path) { 50 | if (_path instanceof RegExp) { 51 | const keys = [] 52 | let name = 0 53 | let m 54 | // eslint-disable-next-line no-cond-assign 55 | while (m = MATCHING_GROUP_REGEXP.exec(_path.source)) { 56 | keys.push({ 57 | name: m[1] || name++, 58 | offset: m.index 59 | }) 60 | } 61 | 62 | return function regexpMatcher (p) { 63 | const match = _path.exec(p) 64 | if (!match) { 65 | return false 66 | } 67 | 68 | const params = {} 69 | for (let i = 1; i < match.length; i++) { 70 | const key = keys[i - 1] 71 | const prop = key.name 72 | const val = decodeParam(match[i]) 73 | 74 | if (val !== undefined) { 75 | params[prop] = val 76 | } 77 | } 78 | 79 | return { 80 | params, 81 | path: match[0] 82 | } 83 | } 84 | } 85 | 86 | return pathRegexp.match((opts.strict ? _path : loosen(_path)), { 87 | sensitive: opts.sensitive, 88 | end: opts.end, 89 | trailing: !opts.strict, 90 | decode: decodeParam 91 | }) 92 | } 93 | this.matchers = Array.isArray(path) ? path.map(matcher) : [matcher(path)] 94 | } 95 | 96 | /** 97 | * Handle the error for the layer. 98 | * 99 | * @param {Error} error 100 | * @param {Request} req 101 | * @param {Response} res 102 | * @param {function} next 103 | * @api private 104 | */ 105 | 106 | Layer.prototype.handleError = function handleError (error, req, res, next) { 107 | const fn = this.handle 108 | 109 | if (fn.length !== 4) { 110 | // not a standard error handler 111 | return next(error) 112 | } 113 | 114 | try { 115 | // invoke function 116 | const ret = fn(error, req, res, next) 117 | 118 | // wait for returned promise 119 | if (isPromise(ret)) { 120 | if (!(ret instanceof Promise)) { 121 | deprecate('handlers that are Promise-like are deprecated, use a native Promise instead') 122 | } 123 | 124 | ret.then(null, function (error) { 125 | next(error || new Error('Rejected promise')) 126 | }) 127 | } 128 | } catch (err) { 129 | next(err) 130 | } 131 | } 132 | 133 | /** 134 | * Handle the request for the layer. 135 | * 136 | * @param {Request} req 137 | * @param {Response} res 138 | * @param {function} next 139 | * @api private 140 | */ 141 | 142 | Layer.prototype.handleRequest = function handleRequest (req, res, next) { 143 | const fn = this.handle 144 | 145 | if (fn.length > 3) { 146 | // not a standard request handler 147 | return next() 148 | } 149 | 150 | try { 151 | // invoke function 152 | const ret = fn(req, res, next) 153 | 154 | // wait for returned promise 155 | if (isPromise(ret)) { 156 | if (!(ret instanceof Promise)) { 157 | deprecate('handlers that are Promise-like are deprecated, use a native Promise instead') 158 | } 159 | 160 | ret.then(null, function (error) { 161 | next(error || new Error('Rejected promise')) 162 | }) 163 | } 164 | } catch (err) { 165 | next(err) 166 | } 167 | } 168 | 169 | /** 170 | * Check if this route matches `path`, if so 171 | * populate `.params`. 172 | * 173 | * @param {String} path 174 | * @return {Boolean} 175 | * @api private 176 | */ 177 | 178 | Layer.prototype.match = function match (path) { 179 | let match 180 | 181 | if (path != null) { 182 | // fast path non-ending match for / (any path matches) 183 | if (this.slash) { 184 | this.params = {} 185 | this.path = '' 186 | return true 187 | } 188 | 189 | let i = 0 190 | while (!match && i < this.matchers.length) { 191 | // match the path 192 | match = this.matchers[i](path) 193 | i++ 194 | } 195 | } 196 | 197 | if (!match) { 198 | this.params = undefined 199 | this.path = undefined 200 | return false 201 | } 202 | 203 | // store values 204 | this.params = match.params 205 | this.path = match.path 206 | this.keys = Object.keys(match.params) 207 | 208 | return true 209 | } 210 | 211 | /** 212 | * Decode param value. 213 | * 214 | * @param {string} val 215 | * @return {string} 216 | * @private 217 | */ 218 | 219 | function decodeParam (val) { 220 | if (typeof val !== 'string' || val.length === 0) { 221 | return val 222 | } 223 | 224 | try { 225 | return decodeURIComponent(val) 226 | } catch (err) { 227 | if (err instanceof URIError) { 228 | err.message = 'Failed to decode param \'' + val + '\'' 229 | err.status = 400 230 | } 231 | 232 | throw err 233 | } 234 | } 235 | 236 | /** 237 | * Loosens the given path for path-to-regexp matching. 238 | */ 239 | function loosen (path) { 240 | if (path instanceof RegExp || path === '/') { 241 | return path 242 | } 243 | 244 | return Array.isArray(path) 245 | ? path.map(function (p) { return loosen(p) }) 246 | : String(path).replace(TRAILING_SLASH_REGEXP, '') 247 | } 248 | -------------------------------------------------------------------------------- /lib/route.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * router 3 | * Copyright(c) 2013 Roman Shtylman 4 | * Copyright(c) 2014-2022 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module dependencies. 12 | * @private 13 | */ 14 | 15 | const debug = require('debug')('router:route') 16 | const Layer = require('./layer') 17 | const { METHODS } = require('node:http') 18 | 19 | /** 20 | * Module variables. 21 | * @private 22 | */ 23 | 24 | const slice = Array.prototype.slice 25 | const flatten = Array.prototype.flat 26 | const methods = METHODS.map((method) => method.toLowerCase()) 27 | 28 | /** 29 | * Expose `Route`. 30 | */ 31 | 32 | module.exports = Route 33 | 34 | /** 35 | * Initialize `Route` with the given `path`, 36 | * 37 | * @param {String} path 38 | * @api private 39 | */ 40 | 41 | function Route (path) { 42 | debug('new %o', path) 43 | this.path = path 44 | this.stack = [] 45 | 46 | // route handlers for various http methods 47 | this.methods = Object.create(null) 48 | } 49 | 50 | /** 51 | * @private 52 | */ 53 | 54 | Route.prototype._handlesMethod = function _handlesMethod (method) { 55 | if (this.methods._all) { 56 | return true 57 | } 58 | 59 | // normalize name 60 | let name = typeof method === 'string' 61 | ? method.toLowerCase() 62 | : method 63 | 64 | if (name === 'head' && !this.methods.head) { 65 | name = 'get' 66 | } 67 | 68 | return Boolean(this.methods[name]) 69 | } 70 | 71 | /** 72 | * @return {array} supported HTTP methods 73 | * @private 74 | */ 75 | 76 | Route.prototype._methods = function _methods () { 77 | const methods = Object.keys(this.methods) 78 | 79 | // append automatic head 80 | if (this.methods.get && !this.methods.head) { 81 | methods.push('head') 82 | } 83 | 84 | for (let i = 0; i < methods.length; i++) { 85 | // make upper case 86 | methods[i] = methods[i].toUpperCase() 87 | } 88 | 89 | return methods 90 | } 91 | 92 | /** 93 | * dispatch req, res into this route 94 | * 95 | * @private 96 | */ 97 | 98 | Route.prototype.dispatch = function dispatch (req, res, done) { 99 | let idx = 0 100 | const stack = this.stack 101 | let sync = 0 102 | 103 | if (stack.length === 0) { 104 | return done() 105 | } 106 | 107 | let method = typeof req.method === 'string' 108 | ? req.method.toLowerCase() 109 | : req.method 110 | 111 | if (method === 'head' && !this.methods.head) { 112 | method = 'get' 113 | } 114 | 115 | req.route = this 116 | 117 | next() 118 | 119 | function next (err) { 120 | // signal to exit route 121 | if (err && err === 'route') { 122 | return done() 123 | } 124 | 125 | // signal to exit router 126 | if (err && err === 'router') { 127 | return done(err) 128 | } 129 | 130 | // no more matching layers 131 | if (idx >= stack.length) { 132 | return done(err) 133 | } 134 | 135 | // max sync stack 136 | if (++sync > 100) { 137 | return setImmediate(next, err) 138 | } 139 | 140 | let layer 141 | let match 142 | 143 | // find next matching layer 144 | while (match !== true && idx < stack.length) { 145 | layer = stack[idx++] 146 | match = !layer.method || layer.method === method 147 | } 148 | 149 | // no match 150 | if (match !== true) { 151 | return done(err) 152 | } 153 | 154 | if (err) { 155 | layer.handleError(err, req, res, next) 156 | } else { 157 | layer.handleRequest(req, res, next) 158 | } 159 | 160 | sync = 0 161 | } 162 | } 163 | 164 | /** 165 | * Add a handler for all HTTP verbs to this route. 166 | * 167 | * Behaves just like middleware and can respond or call `next` 168 | * to continue processing. 169 | * 170 | * You can use multiple `.all` call to add multiple handlers. 171 | * 172 | * function check_something(req, res, next){ 173 | * next() 174 | * } 175 | * 176 | * function validate_user(req, res, next){ 177 | * next() 178 | * } 179 | * 180 | * route 181 | * .all(validate_user) 182 | * .all(check_something) 183 | * .get(function(req, res, next){ 184 | * res.send('hello world') 185 | * }) 186 | * 187 | * @param {array|function} handler 188 | * @return {Route} for chaining 189 | * @api public 190 | */ 191 | 192 | Route.prototype.all = function all (handler) { 193 | const callbacks = flatten.call(slice.call(arguments), Infinity) 194 | 195 | if (callbacks.length === 0) { 196 | throw new TypeError('argument handler is required') 197 | } 198 | 199 | for (let i = 0; i < callbacks.length; i++) { 200 | const fn = callbacks[i] 201 | 202 | if (typeof fn !== 'function') { 203 | throw new TypeError('argument handler must be a function') 204 | } 205 | 206 | const layer = Layer('/', {}, fn) 207 | layer.method = undefined 208 | 209 | this.methods._all = true 210 | this.stack.push(layer) 211 | } 212 | 213 | return this 214 | } 215 | 216 | methods.forEach(function (method) { 217 | Route.prototype[method] = function (handler) { 218 | const callbacks = flatten.call(slice.call(arguments), Infinity) 219 | 220 | if (callbacks.length === 0) { 221 | throw new TypeError('argument handler is required') 222 | } 223 | 224 | for (let i = 0; i < callbacks.length; i++) { 225 | const fn = callbacks[i] 226 | 227 | if (typeof fn !== 'function') { 228 | throw new TypeError('argument handler must be a function') 229 | } 230 | 231 | debug('%s %s', method, this.path) 232 | 233 | const layer = Layer('/', {}, fn) 234 | layer.method = method 235 | 236 | this.methods[method] = true 237 | this.stack.push(layer) 238 | } 239 | 240 | return this 241 | } 242 | }) 243 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "router", 3 | "description": "Simple middleware-style router", 4 | "version": "2.2.0", 5 | "author": "Douglas Christopher Wilson ", 6 | "contributors": [ 7 | "Blake Embrey " 8 | ], 9 | "license": "MIT", 10 | "repository": "pillarjs/router", 11 | "dependencies": { 12 | "debug": "^4.4.0", 13 | "depd": "^2.0.0", 14 | "is-promise": "^4.0.0", 15 | "parseurl": "^1.3.3", 16 | "path-to-regexp": "^8.0.0" 17 | }, 18 | "devDependencies": { 19 | "finalhandler": "^2.1.0", 20 | "mocha": "10.2.0", 21 | "nyc": "15.1.0", 22 | "run-series": "^1.1.9", 23 | "standard": "^17.1.0", 24 | "supertest": "6.3.3" 25 | }, 26 | "files": [ 27 | "lib/", 28 | "LICENSE", 29 | "HISTORY.md", 30 | "README.md", 31 | "index.js" 32 | ], 33 | "engines": { 34 | "node": ">= 18" 35 | }, 36 | "scripts": { 37 | "lint": "standard", 38 | "test": "mocha --reporter spec --bail --check-leaks test/", 39 | "test:debug": "mocha --reporter spec --bail --check-leaks test/ --inspect --inspect-brk", 40 | "test-ci": "nyc --reporter=lcov --reporter=text npm test", 41 | "test-cov": "nyc --reporter=text npm test", 42 | "version": "node scripts/version-history.js && git add HISTORY.md" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/version-history.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const HISTORY_FILE_PATH = path.join(__dirname, '..', 'HISTORY.md') 7 | const MD_HEADER_REGEXP = /^====*$/ 8 | const VERSION = process.env.npm_package_version 9 | const VERSION_PLACEHOLDER_REGEXP = /^(?:unreleased|(\d+\.)+x)$/ 10 | 11 | const historyFileLines = fs.readFileSync(HISTORY_FILE_PATH, 'utf-8').split('\n') 12 | 13 | if (!MD_HEADER_REGEXP.test(historyFileLines[1])) { 14 | console.error('Missing header in HISTORY.md') 15 | process.exit(1) 16 | } 17 | 18 | if (!VERSION_PLACEHOLDER_REGEXP.test(historyFileLines[0])) { 19 | console.error('Missing placeholder version in HISTORY.md') 20 | process.exit(1) 21 | } 22 | 23 | if (historyFileLines[0].indexOf('x') !== -1) { 24 | const versionCheckRegExp = new RegExp('^' + historyFileLines[0].replace('x', '.+') + '$') 25 | 26 | if (!versionCheckRegExp.test(VERSION)) { 27 | console.error('Version %s does not match placeholder %s', VERSION, historyFileLines[0]) 28 | process.exit(1) 29 | } 30 | } 31 | 32 | historyFileLines[0] = VERSION + ' / ' + getLocaleDate() 33 | historyFileLines[1] = repeat('=', historyFileLines[0].length) 34 | 35 | fs.writeFileSync(HISTORY_FILE_PATH, historyFileLines.join('\n')) 36 | 37 | function getLocaleDate () { 38 | const now = new Date() 39 | 40 | return zeroPad(now.getFullYear(), 4) + '-' + 41 | zeroPad(now.getMonth() + 1, 2) + '-' + 42 | zeroPad(now.getDate(), 2) 43 | } 44 | 45 | function repeat (str, length) { 46 | let out = '' 47 | 48 | for (let i = 0; i < length; i++) { 49 | out += str 50 | } 51 | 52 | return out 53 | } 54 | 55 | function zeroPad (number, length) { 56 | let num = number.toString() 57 | 58 | while (num.length < length) { 59 | num = '0' + num 60 | } 61 | 62 | return num 63 | } 64 | -------------------------------------------------------------------------------- /test/auto-head.js: -------------------------------------------------------------------------------- 1 | const { it, describe } = require('mocha') 2 | const Router = require('..') 3 | const utils = require('./support/utils') 4 | 5 | const createServer = utils.createServer 6 | const request = utils.request 7 | 8 | describe('HEAD', function () { 9 | it('should invoke get without head', function (done) { 10 | const router = Router() 11 | const server = createServer(router) 12 | 13 | router.get('/users', sethit(1), saw) 14 | 15 | request(server) 16 | .head('/users') 17 | .expect('Content-Type', 'text/plain') 18 | .expect('x-fn-1', 'hit') 19 | .expect(200, done) 20 | }) 21 | 22 | it('should invoke head if prior to get', function (done) { 23 | const router = Router() 24 | const server = createServer(router) 25 | 26 | router.head('/users', sethit(1), saw) 27 | router.get('/users', sethit(2), saw) 28 | 29 | request(server) 30 | .head('/users') 31 | .expect('Content-Type', 'text/plain') 32 | .expect('x-fn-1', 'hit') 33 | .expect(200, done) 34 | }) 35 | }) 36 | 37 | function saw (req, res) { 38 | const msg = 'saw ' + req.method + ' ' + req.url 39 | res.statusCode = 200 40 | res.setHeader('Content-Type', 'text/plain') 41 | res.end(msg) 42 | } 43 | 44 | function sethit (num) { 45 | const name = 'x-fn-' + String(num) 46 | return function hit (req, res, next) { 47 | res.setHeader(name, 'hit') 48 | next() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/auto-options.js: -------------------------------------------------------------------------------- 1 | const { it, describe } = require('mocha') 2 | const Router = require('..') 3 | const utils = require('./support/utils') 4 | 5 | const createServer = utils.createServer 6 | const request = utils.request 7 | 8 | describe('OPTIONS', function () { 9 | it('should respond with defined routes', function (done) { 10 | const router = Router() 11 | const server = createServer(router) 12 | 13 | router.delete('/', saw) 14 | router.get('/users', saw) 15 | router.post('/users', saw) 16 | router.put('/users', saw) 17 | 18 | request(server) 19 | .options('/users') 20 | .expect('Allow', 'GET, HEAD, POST, PUT') 21 | .expect(200, 'GET, HEAD, POST, PUT', done) 22 | }) 23 | 24 | it('should not contain methods multiple times', function (done) { 25 | const router = Router() 26 | const server = createServer(router) 27 | 28 | router.delete('/', saw) 29 | router.get('/users', saw) 30 | router.put('/users', saw) 31 | router.get('/users', saw) 32 | 33 | request(server) 34 | .options('/users') 35 | .expect('GET, HEAD, PUT') 36 | .expect('Allow', 'GET, HEAD, PUT', done) 37 | }) 38 | 39 | it('should not include "all" routes', function (done) { 40 | const router = Router() 41 | const server = createServer(router) 42 | 43 | router.get('/', saw) 44 | router.get('/users', saw) 45 | router.put('/users', saw) 46 | router.all('/users', sethit(1)) 47 | 48 | request(server) 49 | .options('/users') 50 | .expect('x-fn-1', 'hit') 51 | .expect('Allow', 'GET, HEAD, PUT') 52 | .expect(200, 'GET, HEAD, PUT', done) 53 | }) 54 | 55 | it('should not respond if no matching path', function (done) { 56 | const router = Router() 57 | const server = createServer(router) 58 | 59 | router.get('/users', saw) 60 | 61 | request(server) 62 | .options('/') 63 | .expect(404, done) 64 | }) 65 | 66 | it('should do nothing with explicit options route', function (done) { 67 | const router = Router() 68 | const server = createServer(router) 69 | 70 | router.get('/users', saw) 71 | router.options('/users', saw) 72 | 73 | request(server) 74 | .options('/users') 75 | .expect(200, 'saw OPTIONS /users', done) 76 | }) 77 | 78 | describe('when error occurs in respone handler', function () { 79 | it('should pass error to callback', function (done) { 80 | const router = Router() 81 | const server = createServer(function hander (req, res, next) { 82 | res.writeHead(200) 83 | router(req, res, function (err) { 84 | res.end(String(Boolean(err))) 85 | }) 86 | }) 87 | 88 | router.get('/users', saw) 89 | 90 | request(server) 91 | .options('/users') 92 | .expect(200, 'true', done) 93 | }) 94 | }) 95 | }) 96 | 97 | function saw (req, res) { 98 | const msg = 'saw ' + req.method + ' ' + req.url 99 | res.statusCode = 200 100 | res.setHeader('Content-Type', 'text/plain') 101 | res.end(msg) 102 | } 103 | 104 | function sethit (num) { 105 | const name = 'x-fn-' + String(num) 106 | return function hit (req, res, next) { 107 | res.setHeader(name, 'hit') 108 | next() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /test/fqdn-url.js: -------------------------------------------------------------------------------- 1 | const { it, describe } = require('mocha') 2 | const Router = require('..') 3 | const utils = require('./support/utils') 4 | 5 | const createServer = utils.createServer 6 | const rawrequest = utils.rawrequest 7 | 8 | describe('FQDN url', function () { 9 | it('should not obscure FQDNs', function (done) { 10 | const router = new Router() 11 | const server = createServer(router) 12 | 13 | router.use(saw) 14 | 15 | rawrequest(server) 16 | .get('http://example.com/foo') 17 | .expect(200, 'saw GET http://example.com/foo', done) 18 | }) 19 | 20 | it('should strip/restore FQDN req.url', function (done) { 21 | const router = new Router() 22 | const server = createServer(router) 23 | 24 | router.use('/blog', setsaw(1)) 25 | router.use(saw) 26 | 27 | rawrequest(server) 28 | .get('http://example.com/blog/post/1') 29 | .expect('x-saw-1', 'GET http://example.com/post/1') 30 | .expect(200, 'saw GET http://example.com/blog/post/1', done) 31 | }) 32 | 33 | it('should ignore FQDN in search', function (done) { 34 | const router = new Router() 35 | const server = createServer(router) 36 | 37 | router.use('/proxy', setsaw(1)) 38 | router.use(saw) 39 | 40 | rawrequest(server) 41 | .get('/proxy?url=http://example.com/blog/post/1') 42 | .expect('x-saw-1', 'GET /?url=http://example.com/blog/post/1') 43 | .expect(200, 'saw GET /proxy?url=http://example.com/blog/post/1', done) 44 | }) 45 | 46 | it('should ignore FQDN in path', function (done) { 47 | const router = new Router() 48 | const server = createServer(router) 49 | 50 | router.use('/proxy', setsaw(1)) 51 | router.use(saw) 52 | 53 | rawrequest(server) 54 | .get('/proxy/http://example.com/blog/post/1') 55 | .expect('x-saw-1', 'GET /http://example.com/blog/post/1') 56 | .expect(200, 'saw GET /proxy/http://example.com/blog/post/1', done) 57 | }) 58 | }) 59 | 60 | function setsaw (num) { 61 | const name = 'x-saw-' + String(num) 62 | return function hit (req, res, next) { 63 | res.setHeader(name, req.method + ' ' + req.url) 64 | next() 65 | } 66 | } 67 | 68 | function saw (req, res) { 69 | const msg = 'saw ' + req.method + ' ' + req.url 70 | res.statusCode = 200 71 | res.setHeader('Content-Type', 'text/plain') 72 | res.end(msg) 73 | } 74 | -------------------------------------------------------------------------------- /test/param.js: -------------------------------------------------------------------------------- 1 | const { it, describe } = require('mocha') 2 | const series = require('run-series') 3 | const Router = require('..') 4 | const utils = require('./support/utils') 5 | 6 | const assert = utils.assert 7 | const createHitHandle = utils.createHitHandle 8 | const shouldHitHandle = utils.shouldHitHandle 9 | const shouldNotHitHandle = utils.shouldNotHitHandle 10 | const createServer = utils.createServer 11 | const request = utils.request 12 | 13 | describe('Router', function () { 14 | describe('.param(name, fn)', function () { 15 | it('should reject missing name', function () { 16 | const router = new Router() 17 | assert.throws(router.param.bind(router), /argument name is required/) 18 | }) 19 | 20 | it('should reject bad name', function () { 21 | const router = new Router() 22 | assert.throws(router.param.bind(router, 42), /argument name must be a string/) 23 | }) 24 | 25 | it('should reject missing fn', function () { 26 | const router = new Router() 27 | assert.throws(router.param.bind(router, 'id'), /argument fn is required/) 28 | }) 29 | 30 | it('should reject bad fn', function () { 31 | const router = new Router() 32 | assert.throws(router.param.bind(router, 'id', 42), /argument fn must be a function/) 33 | }) 34 | 35 | it('should map logic for a path param', function (done) { 36 | const router = new Router() 37 | const server = createServer(router) 38 | 39 | router.param('id', function parseId (req, res, next, val) { 40 | req.params.id = Number(val) 41 | next() 42 | }) 43 | 44 | router.get('/user/:id', function (req, res) { 45 | res.setHeader('Content-Type', 'text/plain') 46 | res.end('get user ' + req.params.id) 47 | }) 48 | 49 | series([ 50 | function (cb) { 51 | request(server) 52 | .get('/user/2') 53 | .expect(200, 'get user 2', cb) 54 | }, 55 | function (cb) { 56 | request(server) 57 | .get('/user/bob') 58 | .expect(200, 'get user NaN', cb) 59 | } 60 | ], done) 61 | }) 62 | 63 | it('should allow chaining', function (done) { 64 | const router = new Router() 65 | const server = createServer(router) 66 | 67 | router.param('id', function parseId (req, res, next, val) { 68 | req.params.id = Number(val) 69 | next() 70 | }) 71 | 72 | router.param('id', function parseId (req, res, next, val) { 73 | req.itemId = Number(val) 74 | next() 75 | }) 76 | 77 | router.get('/user/:id', function (req, res) { 78 | res.setHeader('Content-Type', 'text/plain') 79 | res.end('get user ' + req.params.id + ' (' + req.itemId + ')') 80 | }) 81 | 82 | request(server) 83 | .get('/user/2') 84 | .expect(200, 'get user 2 (2)', done) 85 | }) 86 | 87 | it('should automatically decode path value', function (done) { 88 | const router = new Router() 89 | const server = createServer(router) 90 | 91 | router.param('user', function parseUser (req, res, next, user) { 92 | req.user = user 93 | next() 94 | }) 95 | 96 | router.get('/user/:id', function (req, res) { 97 | res.setHeader('Content-Type', 'text/plain') 98 | res.end('get user ' + req.params.id) 99 | }) 100 | 101 | request(server) 102 | .get('/user/%22bob%2Frobert%22') 103 | .expect('get user "bob/robert"', done) 104 | }) 105 | 106 | it('should 400 on invalid path value', function (done) { 107 | const router = new Router() 108 | const server = createServer(router) 109 | 110 | router.param('user', function parseUser (req, res, next, user) { 111 | req.user = user 112 | next() 113 | }) 114 | 115 | router.get('/user/:id', function (req, res) { 116 | res.setHeader('Content-Type', 'text/plain') 117 | res.end('get user ' + req.params.id) 118 | }) 119 | 120 | request(server) 121 | .get('/user/%bob') 122 | .expect(400, /URIError: Failed to decode param/, done) 123 | }) 124 | 125 | it('should only invoke fn when necessary', function (done) { 126 | const router = new Router() 127 | const server = createServer(router) 128 | 129 | router.param('id', function parseId (req, res, next, val) { 130 | res.setHeader('x-id', val) 131 | next() 132 | }) 133 | 134 | router.param('user', function parseUser (req, res, next, user) { 135 | throw new Error('boom') 136 | }) 137 | 138 | router.get('/user/:user', saw) 139 | router.put('/user/:id', saw) 140 | 141 | series([ 142 | function (cb) { 143 | request(server) 144 | .get('/user/bob') 145 | .expect(500, /Error: boom/, cb) 146 | }, 147 | function (cb) { 148 | request(server) 149 | .put('/user/bob') 150 | .expect('x-id', 'bob') 151 | .expect(200, 'saw PUT /user/bob', cb) 152 | } 153 | ], done) 154 | }) 155 | 156 | it('should only invoke fn once per request', function (done) { 157 | const router = new Router() 158 | const server = createServer(router) 159 | 160 | router.param('user', function parseUser (req, res, next, user) { 161 | req.count = (req.count || 0) + 1 162 | req.user = user 163 | next() 164 | }) 165 | 166 | router.get('/user/:user', sethit(1)) 167 | router.get('/user/:user', sethit(2)) 168 | 169 | router.use(function (req, res) { 170 | res.end('get user ' + req.user + ' ' + req.count + ' times') 171 | }) 172 | 173 | request(server) 174 | .get('/user/bob') 175 | .expect('get user bob 1 times', done) 176 | }) 177 | 178 | it('should keep changes to req.params value', function (done) { 179 | const router = new Router() 180 | const server = createServer(router) 181 | 182 | router.param('id', function parseUser (req, res, next, val) { 183 | req.count = (req.count || 0) + 1 184 | req.params.id = Number(val) 185 | next() 186 | }) 187 | 188 | router.get('/user/:id', function (req, res, next) { 189 | res.setHeader('x-user-id', req.params.id) 190 | next() 191 | }) 192 | 193 | router.get('/user/:id', function (req, res) { 194 | res.end('get user ' + req.params.id + ' ' + req.count + ' times') 195 | }) 196 | 197 | request(server) 198 | .get('/user/01') 199 | .expect('get user 1 1 times', done) 200 | }) 201 | 202 | it('should invoke fn if path value differs', function (done) { 203 | const router = new Router() 204 | const server = createServer(router) 205 | 206 | router.param('user', function parseUser (req, res, next, user) { 207 | req.count = (req.count || 0) + 1 208 | req.user = user 209 | req.vals = (req.vals || []).concat(user) 210 | next() 211 | }) 212 | 213 | router.get('/:user/bob', sethit(1)) 214 | router.get('/user/:user', sethit(2)) 215 | 216 | router.use(function (req, res) { 217 | res.end('get user ' + req.user + ' ' + req.count + ' times: ' + req.vals.join(', ')) 218 | }) 219 | 220 | request(server) 221 | .get('/user/bob') 222 | .expect('get user bob 2 times: user, bob', done) 223 | }) 224 | 225 | it('should catch exception in fn', function (done) { 226 | const router = new Router() 227 | const server = createServer(router) 228 | 229 | router.param('user', function parseUser (req, res, next, user) { 230 | throw new Error('boom') 231 | }) 232 | 233 | router.get('/user/:user', function (req, res) { 234 | res.setHeader('Content-Type', 'text/plain') 235 | res.end('get user ' + req.params.id) 236 | }) 237 | 238 | request(server) 239 | .get('/user/bob') 240 | .expect(500, /Error: boom/, done) 241 | }) 242 | 243 | it('should catch exception in chained fn', function (done) { 244 | const router = new Router() 245 | const server = createServer(router) 246 | 247 | router.param('user', function parseUser (req, res, next, user) { 248 | process.nextTick(next) 249 | }) 250 | 251 | router.param('user', function parseUser (req, res, next, user) { 252 | throw new Error('boom') 253 | }) 254 | 255 | router.get('/user/:user', function (req, res) { 256 | res.setHeader('Content-Type', 'text/plain') 257 | res.end('get user ' + req.params.id) 258 | }) 259 | 260 | request(server) 261 | .get('/user/bob') 262 | .expect(500, /Error: boom/, done) 263 | }) 264 | 265 | describe('promise support', function () { 266 | it('should pass rejected promise value', function (done) { 267 | const router = new Router() 268 | const server = createServer(router) 269 | 270 | router.param('user', function parseUser (req, res, next, user) { 271 | return Promise.reject(new Error('boom')) 272 | }) 273 | 274 | router.get('/user/:user', function (req, res) { 275 | res.setHeader('Content-Type', 'text/plain') 276 | res.end('get user ' + req.params.id) 277 | }) 278 | 279 | request(server) 280 | .get('/user/bob') 281 | .expect(500, /Error: boom/, done) 282 | }) 283 | 284 | it('should pass rejected promise without value', function (done) { 285 | const router = new Router() 286 | const server = createServer(router) 287 | 288 | router.use(function createError (req, res, next) { 289 | return Promise.reject() // eslint-disable-line prefer-promise-reject-errors 290 | }) 291 | 292 | router.param('user', function parseUser (req, res, next, user) { 293 | return Promise.reject() // eslint-disable-line prefer-promise-reject-errors 294 | }) 295 | 296 | router.get('/user/:user', function (req, res) { 297 | res.setHeader('Content-Type', 'text/plain') 298 | res.end('get user ' + req.params.id) 299 | }) 300 | 301 | request(server) 302 | .get('/user/bob') 303 | .expect(500, /Error: Rejected promise/, done) 304 | }) 305 | }) 306 | 307 | describe('next("route")', function () { 308 | it('should cause route with param to be skipped', function (done) { 309 | const router = new Router() 310 | const server = createServer(router) 311 | 312 | router.param('id', function parseId (req, res, next, val) { 313 | const id = Number(val) 314 | 315 | if (isNaN(id)) { 316 | return next('route') 317 | } 318 | 319 | req.params.id = id 320 | next() 321 | }) 322 | 323 | router.get('/user/:id', function (req, res) { 324 | res.setHeader('Content-Type', 'text/plain') 325 | res.end('get user ' + req.params.id) 326 | }) 327 | 328 | router.get('/user/new', function (req, res) { 329 | res.statusCode = 400 330 | res.setHeader('Content-Type', 'text/plain') 331 | res.end('cannot get a new user') 332 | }) 333 | 334 | series([ 335 | function (cb) { 336 | request(server) 337 | .get('/user/2') 338 | .expect(200, 'get user 2', cb) 339 | }, 340 | function (cb) { 341 | request(server) 342 | .get('/user/bob') 343 | .expect(404, cb) 344 | }, 345 | function (cb) { 346 | request(server) 347 | .get('/user/new') 348 | .expect(400, 'cannot get a new user', cb) 349 | } 350 | ], done) 351 | }) 352 | 353 | it('should invoke fn if path value differs', function (done) { 354 | const router = new Router() 355 | const server = createServer(router) 356 | 357 | router.param('user', function parseUser (req, res, next, user) { 358 | req.count = (req.count || 0) + 1 359 | req.user = user 360 | req.vals = (req.vals || []).concat(user) 361 | next(user === 'user' ? 'route' : null) 362 | }) 363 | 364 | router.get('/:user/bob', createHitHandle(1)) 365 | router.get('/user/:user', createHitHandle(2)) 366 | 367 | router.use(function (req, res) { 368 | res.end('get user ' + req.user + ' ' + req.count + ' times: ' + req.vals.join(', ')) 369 | }) 370 | 371 | request(server) 372 | .get('/user/bob') 373 | .expect(shouldNotHitHandle(1)) 374 | .expect(shouldHitHandle(2)) 375 | .expect('get user bob 2 times: user, bob', done) 376 | }) 377 | }) 378 | }) 379 | }) 380 | 381 | function sethit (num) { 382 | const name = 'x-fn-' + String(num) 383 | return function hit (req, res, next) { 384 | res.setHeader(name, 'hit') 385 | next() 386 | } 387 | } 388 | 389 | function saw (req, res) { 390 | const msg = 'saw ' + req.method + ' ' + req.url 391 | res.statusCode = 200 392 | res.setHeader('Content-Type', 'text/plain') 393 | res.end(msg) 394 | } 395 | -------------------------------------------------------------------------------- /test/req.params.js: -------------------------------------------------------------------------------- 1 | const { it, describe } = require('mocha') 2 | const Router = require('..') 3 | const utils = require('./support/utils') 4 | 5 | const createServer = utils.createServer 6 | const request = utils.request 7 | 8 | describe('req.params', function () { 9 | it('should default to empty object', function (done) { 10 | const router = Router() 11 | const server = createServer(router) 12 | 13 | router.get('/', sawParams) 14 | 15 | request(server) 16 | .get('/') 17 | .expect(200, '{}', done) 18 | }) 19 | 20 | it('should not exist outside the router', function (done) { 21 | const router = Router() 22 | const server = createServer(function (req, res, next) { 23 | router(req, res, function (err) { 24 | if (err) return next(err) 25 | sawParams(req, res) 26 | }) 27 | }) 28 | 29 | router.get('/', hitParams(1)) 30 | 31 | request(server) 32 | .get('/') 33 | .expect('x-params-1', '{}') 34 | .expect(200, '', done) 35 | }) 36 | 37 | it('should overwrite value outside the router', function (done) { 38 | const router = Router() 39 | const server = createServer(function (req, res, next) { 40 | req.params = { foo: 'bar' } 41 | router(req, res, done) 42 | }) 43 | 44 | router.get('/', sawParams) 45 | 46 | request(server) 47 | .get('/') 48 | .expect(200, '{}', done) 49 | }) 50 | 51 | it('should restore previous value outside the router', function (done) { 52 | const router = Router() 53 | const server = createServer(function (req, res, next) { 54 | req.params = { foo: 'bar' } 55 | 56 | router(req, res, function (err) { 57 | if (err) return next(err) 58 | sawParams(req, res) 59 | }) 60 | }) 61 | 62 | router.get('/', hitParams(1)) 63 | 64 | request(server) 65 | .get('/') 66 | .expect('x-params-1', '{}') 67 | .expect(200, '{"foo":"bar"}', done) 68 | }) 69 | 70 | describe('when "mergeParams: true"', function () { 71 | it('should merge outside object with params', function (done) { 72 | const router = Router({ mergeParams: true }) 73 | const server = createServer(function (req, res, next) { 74 | req.params = { foo: 'bar' } 75 | 76 | router(req, res, function (err) { 77 | if (err) return next(err) 78 | sawParams(req, res) 79 | }) 80 | }) 81 | 82 | router.get('/:fizz', hitParams(1)) 83 | 84 | request(server) 85 | .get('/buzz') 86 | .expect('x-params-1', '{"foo":"bar","fizz":"buzz"}') 87 | .expect(200, '{"foo":"bar"}', done) 88 | }) 89 | 90 | it('should ignore non-object outside object', function (done) { 91 | const router = Router({ mergeParams: true }) 92 | const server = createServer(function (req, res, next) { 93 | req.params = 42 94 | 95 | router(req, res, function (err) { 96 | if (err) return next(err) 97 | sawParams(req, res) 98 | }) 99 | }) 100 | 101 | router.get('/:fizz', hitParams(1)) 102 | 103 | request(server) 104 | .get('/buzz') 105 | .expect('x-params-1', '{"fizz":"buzz"}') 106 | .expect(200, '42', done) 107 | }) 108 | 109 | it('should overwrite outside keys that are the same', function (done) { 110 | const router = Router({ mergeParams: true }) 111 | const server = createServer(function (req, res, next) { 112 | req.params = { foo: 'bar' } 113 | 114 | router(req, res, function (err) { 115 | if (err) return next(err) 116 | sawParams(req, res) 117 | }) 118 | }) 119 | 120 | router.get('/:foo', hitParams(1)) 121 | 122 | request(server) 123 | .get('/buzz') 124 | .expect('x-params-1', '{"foo":"buzz"}') 125 | .expect(200, '{"foo":"bar"}', done) 126 | }) 127 | 128 | describe('with numeric properties in req.params', function () { 129 | it('should merge numeric properties by offsetting', function (done) { 130 | const router = Router({ mergeParams: true }) 131 | const server = createServer(function (req, res, next) { 132 | req.params = { 0: 'foo', 1: 'bar' } 133 | 134 | router(req, res, function (err) { 135 | if (err) return next(err) 136 | sawParams(req, res) 137 | }) 138 | }) 139 | 140 | router.get(/\/([^/]*)/, hitParams(1)) 141 | 142 | request(server) 143 | .get('/buzz') 144 | .expect('x-params-1', '{"0":"foo","1":"bar","2":"buzz"}') 145 | .expect(200, '{"0":"foo","1":"bar"}', done) 146 | }) 147 | 148 | it('should merge with same numeric properties', function (done) { 149 | const router = Router({ mergeParams: true }) 150 | const server = createServer(function (req, res, next) { 151 | req.params = { 0: 'foo' } 152 | 153 | router(req, res, function (err) { 154 | if (err) return next(err) 155 | sawParams(req, res) 156 | }) 157 | }) 158 | 159 | router.get(/\/([^/]*)/, hitParams(1)) 160 | 161 | request(server) 162 | .get('/bar') 163 | .expect('x-params-1', '{"0":"foo","1":"bar"}') 164 | .expect(200, '{"0":"foo"}', done) 165 | }) 166 | }) 167 | }) 168 | }) 169 | 170 | function hitParams (num) { 171 | const name = 'x-params-' + String(num) 172 | return function hit (req, res, next) { 173 | res.setHeader(name, JSON.stringify(req.params)) 174 | next() 175 | } 176 | } 177 | 178 | function sawParams (req, res) { 179 | res.statusCode = 200 180 | res.setHeader('Content-Type', 'application/json') 181 | res.end(JSON.stringify(req.params)) 182 | } 183 | -------------------------------------------------------------------------------- /test/route.js: -------------------------------------------------------------------------------- 1 | const { it, describe } = require('mocha') 2 | const series = require('run-series') 3 | const Router = require('..') 4 | const utils = require('./support/utils') 5 | 6 | const assert = utils.assert 7 | const createHitHandle = utils.createHitHandle 8 | const createServer = utils.createServer 9 | const request = utils.request 10 | const shouldHaveBody = utils.shouldHaveBody 11 | const shouldHitHandle = utils.shouldHitHandle 12 | const shouldNotHaveBody = utils.shouldNotHaveBody 13 | const shouldNotHitHandle = utils.shouldNotHitHandle 14 | const methods = utils.methods 15 | 16 | describe('Router', function () { 17 | describe('.route(path)', function () { 18 | it('should return a new route', function () { 19 | const router = new Router() 20 | const route = router.route('/foo') 21 | assert.equal(route.path, '/foo') 22 | }) 23 | 24 | it('should respond to multiple methods', function (done) { 25 | const router = new Router() 26 | const route = router.route('/foo') 27 | const server = createServer(router) 28 | 29 | route.get(saw) 30 | route.post(saw) 31 | 32 | series([ 33 | function (cb) { 34 | request(server) 35 | .get('/foo') 36 | .expect(200, 'saw GET /foo', cb) 37 | }, 38 | function (cb) { 39 | request(server) 40 | .post('/foo') 41 | .expect(200, 'saw POST /foo', cb) 42 | }, 43 | function (cb) { 44 | request(server) 45 | .put('/foo') 46 | .expect(404, cb) 47 | } 48 | ], done) 49 | }) 50 | 51 | it('should route without method', function (done) { 52 | const router = new Router() 53 | const route = router.route('/foo') 54 | const server = createServer(function (req, res, next) { 55 | req.method = undefined 56 | router(req, res, next) 57 | }) 58 | 59 | route.post(createHitHandle(1)) 60 | route.all(createHitHandle(2)) 61 | route.get(createHitHandle(3)) 62 | 63 | router.get('/foo', createHitHandle(4)) 64 | router.use(saw) 65 | 66 | request(server) 67 | .get('/foo') 68 | .expect(shouldNotHitHandle(1)) 69 | .expect(shouldHitHandle(2)) 70 | .expect(shouldNotHitHandle(3)) 71 | .expect(shouldNotHitHandle(4)) 72 | .expect(200, 'saw undefined /foo', done) 73 | }) 74 | 75 | it('should stack', function (done) { 76 | const router = new Router() 77 | const route = router.route('/foo') 78 | const server = createServer(router) 79 | 80 | route.post(createHitHandle(1)) 81 | route.all(createHitHandle(2)) 82 | route.get(createHitHandle(3)) 83 | 84 | router.use(saw) 85 | 86 | series([ 87 | function (cb) { 88 | request(server) 89 | .get('/foo') 90 | .expect('x-fn-2', 'hit') 91 | .expect('x-fn-3', 'hit') 92 | .expect(200, 'saw GET /foo', cb) 93 | }, 94 | function (cb) { 95 | request(server) 96 | .post('/foo') 97 | .expect('x-fn-1', 'hit') 98 | .expect('x-fn-2', 'hit') 99 | .expect(200, 'saw POST /foo', cb) 100 | }, 101 | function (cb) { 102 | request(server) 103 | .put('/foo') 104 | .expect('x-fn-2', 'hit') 105 | .expect(200, 'saw PUT /foo', cb) 106 | } 107 | ], done) 108 | }) 109 | 110 | it('should not error on empty route', function (done) { 111 | const router = new Router() 112 | const route = router.route('/foo') 113 | const server = createServer(router) 114 | 115 | assert.ok(route) 116 | 117 | series([ 118 | function (cb) { 119 | request(server) 120 | .get('/foo') 121 | .expect(404, cb) 122 | }, 123 | function (cb) { 124 | request(server) 125 | .head('/foo') 126 | .expect(404, cb) 127 | } 128 | ], done) 129 | }) 130 | 131 | it('should not invoke singular error route', function (done) { 132 | const router = new Router() 133 | const route = router.route('/foo') 134 | const server = createServer(router) 135 | 136 | route.all(function handleError (err, req, res, next) { 137 | throw err || new Error('boom!') 138 | }) 139 | 140 | request(server) 141 | .get('/foo') 142 | .expect(404, done) 143 | }) 144 | 145 | it('should not stack overflow with a large sync stack', function (done) { 146 | this.timeout(5000) // long-running test 147 | 148 | const router = new Router() 149 | const route = router.route('/foo') 150 | const server = createServer(router) 151 | 152 | for (let i = 0; i < 6000; i++) { 153 | route.all(function (req, res, next) { next() }) 154 | } 155 | 156 | route.get(helloWorld) 157 | 158 | request(server) 159 | .get('/foo') 160 | .expect(200, 'hello, world', done) 161 | }) 162 | 163 | describe('.all(...fn)', function () { 164 | it('should reject no arguments', function () { 165 | const router = new Router() 166 | const route = router.route('/') 167 | assert.throws(route.all.bind(route), /argument handler is required/) 168 | }) 169 | 170 | it('should reject empty array', function () { 171 | const router = new Router() 172 | const route = router.route('/') 173 | assert.throws(route.all.bind(route, []), /argument handler is required/) 174 | }) 175 | 176 | it('should reject invalid fn', function () { 177 | const router = new Router() 178 | const route = router.route('/') 179 | assert.throws(route.all.bind(route, 2), /argument handler must be a function/) 180 | }) 181 | 182 | it('should respond to all methods', function (done) { 183 | const router = new Router() 184 | const route = router.route('/foo') 185 | const server = createServer(router) 186 | 187 | route.all(saw) 188 | 189 | series([ 190 | function (cb) { 191 | request(server) 192 | .get('/foo') 193 | .expect(200, 'saw GET /foo', cb) 194 | }, 195 | function (cb) { 196 | request(server) 197 | .post('/foo') 198 | .expect(200, 'saw POST /foo', cb) 199 | }, 200 | function (cb) { 201 | request(server) 202 | .put('/foo') 203 | .expect(200, 'saw PUT /foo', cb) 204 | } 205 | ], done) 206 | }) 207 | 208 | it('should accept multiple arguments', function (done) { 209 | const router = new Router() 210 | const route = router.route('/foo') 211 | const server = createServer(router) 212 | 213 | route.all(createHitHandle(1), createHitHandle(2), helloWorld) 214 | 215 | request(server) 216 | .get('/foo') 217 | .expect('x-fn-1', 'hit') 218 | .expect('x-fn-2', 'hit') 219 | .expect(200, 'hello, world', done) 220 | }) 221 | 222 | it('should accept single array of handlers', function (done) { 223 | const router = new Router() 224 | const route = router.route('/foo') 225 | const server = createServer(router) 226 | 227 | route.all([createHitHandle(1), createHitHandle(2), helloWorld]) 228 | 229 | request(server) 230 | .get('/foo') 231 | .expect('x-fn-1', 'hit') 232 | .expect('x-fn-2', 'hit') 233 | .expect(200, 'hello, world', done) 234 | }) 235 | 236 | it('should accept nested arrays of handlers', function (done) { 237 | const router = new Router() 238 | const route = router.route('/foo') 239 | const server = createServer(router) 240 | 241 | route.all([[createHitHandle(1), createHitHandle(2)], createHitHandle(3)], helloWorld) 242 | 243 | request(server) 244 | .get('/foo') 245 | .expect('x-fn-1', 'hit') 246 | .expect('x-fn-2', 'hit') 247 | .expect('x-fn-3', 'hit') 248 | .expect(200, 'hello, world', done) 249 | }) 250 | }) 251 | 252 | methods.slice().sort().forEach(function (method) { 253 | if (method === 'connect') { 254 | // CONNECT is tricky and supertest doesn't support it 255 | return 256 | } 257 | if (method === 'query' && process.version.startsWith('v21')) { 258 | return 259 | } 260 | 261 | const body = method !== 'head' 262 | ? shouldHaveBody(Buffer.from('hello, world')) 263 | : shouldNotHaveBody() 264 | 265 | describe('.' + method + '(...fn)', function () { 266 | it('should respond to a ' + method.toUpperCase() + ' request', function (done) { 267 | const router = new Router() 268 | const route = router.route('/') 269 | const server = createServer(router) 270 | 271 | route[method](helloWorld) 272 | 273 | request(server)[method]('/') 274 | .expect(200) 275 | .expect(body) 276 | .end(done) 277 | }) 278 | 279 | it('should reject no arguments', function () { 280 | const router = new Router() 281 | const route = router.route('/') 282 | assert.throws(route[method].bind(route), /argument handler is required/) 283 | }) 284 | 285 | it('should reject empty array', function () { 286 | const router = new Router() 287 | const route = router.route('/') 288 | assert.throws(route[method].bind(route, []), /argument handler is required/) 289 | }) 290 | 291 | it('should reject invalid fn', function () { 292 | const router = new Router() 293 | const route = router.route('/') 294 | assert.throws(route[method].bind(route, 2), /argument handler must be a function/) 295 | }) 296 | 297 | it('should accept multiple arguments', function (done) { 298 | const router = new Router() 299 | const route = router.route('/foo') 300 | const server = createServer(router) 301 | 302 | route[method](createHitHandle(1), createHitHandle(2), helloWorld) 303 | 304 | request(server)[method]('/foo') 305 | .expect(200) 306 | .expect('x-fn-1', 'hit') 307 | .expect('x-fn-2', 'hit') 308 | .expect(body) 309 | .end(done) 310 | }) 311 | 312 | it('should accept single array of handlers', function (done) { 313 | const router = new Router() 314 | const route = router.route('/foo') 315 | const server = createServer(router) 316 | 317 | route[method]([createHitHandle(1), createHitHandle(2), helloWorld]) 318 | 319 | request(server)[method]('/foo') 320 | .expect(200) 321 | .expect('x-fn-1', 'hit') 322 | .expect('x-fn-2', 'hit') 323 | .expect(body) 324 | .end(done) 325 | }) 326 | 327 | it('should accept nested arrays of handlers', function (done) { 328 | const router = new Router() 329 | const route = router.route('/foo') 330 | const server = createServer(router) 331 | 332 | route[method]([[createHitHandle(1), createHitHandle(2)], createHitHandle(3)], helloWorld) 333 | 334 | request(server)[method]('/foo') 335 | .expect(200) 336 | .expect('x-fn-1', 'hit') 337 | .expect('x-fn-2', 'hit') 338 | .expect('x-fn-3', 'hit') 339 | .expect(body) 340 | .end(done) 341 | }) 342 | }) 343 | }) 344 | 345 | describe('error handling', function () { 346 | it('should handle errors from next(err)', function (done) { 347 | const router = new Router() 348 | const route = router.route('/foo') 349 | const server = createServer(router) 350 | 351 | route.all(function createError (req, res, next) { 352 | next(new Error('boom!')) 353 | }) 354 | 355 | route.all(helloWorld) 356 | 357 | route.all(function handleError (err, req, res, next) { 358 | res.statusCode = 500 359 | res.end('caught: ' + err.message) 360 | }) 361 | 362 | request(server) 363 | .get('/foo') 364 | .expect(500, 'caught: boom!', done) 365 | }) 366 | 367 | it('should handle errors thrown', function (done) { 368 | const router = new Router() 369 | const route = router.route('/foo') 370 | const server = createServer(router) 371 | 372 | route.all(function createError (req, res, next) { 373 | throw new Error('boom!') 374 | }) 375 | 376 | route.all(helloWorld) 377 | 378 | route.all(function handleError (err, req, res, next) { 379 | res.statusCode = 500 380 | res.end('caught: ' + err.message) 381 | }) 382 | 383 | request(server) 384 | .get('/foo') 385 | .expect(500, 'caught: boom!', done) 386 | }) 387 | 388 | it('should handle errors thrown in error handlers', function (done) { 389 | const router = new Router() 390 | const route = router.route('/foo') 391 | const server = createServer(router) 392 | 393 | route.all(function createError (req, res, next) { 394 | throw new Error('boom!') 395 | }) 396 | 397 | route.all(function handleError (err, req, res, next) { 398 | throw new Error('ouch: ' + err.message) 399 | }) 400 | 401 | route.all(function handleError (err, req, res, next) { 402 | res.statusCode = 500 403 | res.end('caught: ' + err.message) 404 | }) 405 | 406 | request(server) 407 | .get('/foo') 408 | .expect(500, 'caught: ouch: boom!', done) 409 | }) 410 | }) 411 | 412 | describe('next("route")', function () { 413 | it('should invoke next handler', function (done) { 414 | const router = new Router() 415 | const route = router.route('/foo') 416 | const server = createServer(router) 417 | 418 | route.get(function handle (req, res, next) { 419 | res.setHeader('x-next', 'route') 420 | next('route') 421 | }) 422 | 423 | router.use(saw) 424 | 425 | request(server) 426 | .get('/foo') 427 | .expect('x-next', 'route') 428 | .expect(200, 'saw GET /foo', done) 429 | }) 430 | 431 | it('should invoke next route', function (done) { 432 | const router = new Router() 433 | const route = router.route('/foo') 434 | const server = createServer(router) 435 | 436 | route.get(function handle (req, res, next) { 437 | res.setHeader('x-next', 'route') 438 | next('route') 439 | }) 440 | 441 | router.route('/foo').all(saw) 442 | 443 | request(server) 444 | .get('/foo') 445 | .expect('x-next', 'route') 446 | .expect(200, 'saw GET /foo', done) 447 | }) 448 | 449 | it('should skip next handlers in route', function (done) { 450 | const router = new Router() 451 | const route = router.route('/foo') 452 | const server = createServer(router) 453 | 454 | route.all(createHitHandle(1)) 455 | route.get(function goNext (req, res, next) { 456 | res.setHeader('x-next', 'route') 457 | next('route') 458 | }) 459 | route.all(createHitHandle(2)) 460 | 461 | router.use(saw) 462 | 463 | request(server) 464 | .get('/foo') 465 | .expect(shouldHitHandle(1)) 466 | .expect('x-next', 'route') 467 | .expect(shouldNotHitHandle(2)) 468 | .expect(200, 'saw GET /foo', done) 469 | }) 470 | 471 | it('should not invoke error handlers', function (done) { 472 | const router = new Router() 473 | const route = router.route('/foo') 474 | const server = createServer(router) 475 | 476 | route.all(function goNext (req, res, next) { 477 | res.setHeader('x-next', 'route') 478 | next('route') 479 | }) 480 | 481 | route.all(function handleError (err, req, res, next) { 482 | res.statusCode = 500 483 | res.end('caught: ' + err.message) 484 | }) 485 | 486 | request(server) 487 | .get('/foo') 488 | .expect('x-next', 'route') 489 | .expect(404, done) 490 | }) 491 | }) 492 | 493 | describe('next("router")', function () { 494 | it('should exit the router', function (done) { 495 | const router = new Router() 496 | const route = router.route('/foo') 497 | const server = createServer(router) 498 | 499 | function handle (req, res, next) { 500 | res.setHeader('x-next', 'router') 501 | next('router') 502 | } 503 | 504 | route.get(handle, createHitHandle(1)) 505 | 506 | router.use(saw) 507 | 508 | request(server) 509 | .get('/foo') 510 | .expect('x-next', 'router') 511 | .expect(shouldNotHitHandle(1)) 512 | .expect(404, done) 513 | }) 514 | 515 | it('should not invoke error handlers', function (done) { 516 | const router = new Router() 517 | const route = router.route('/foo') 518 | const server = createServer(router) 519 | 520 | route.all(function goNext (req, res, next) { 521 | res.setHeader('x-next', 'router') 522 | next('router') 523 | }) 524 | 525 | route.all(function handleError (err, req, res, next) { 526 | res.statusCode = 500 527 | res.end('caught: ' + err.message) 528 | }) 529 | 530 | router.use(function handleError (err, req, res, next) { 531 | res.statusCode = 500 532 | res.end('caught: ' + err.message) 533 | }) 534 | 535 | request(server) 536 | .get('/foo') 537 | .expect('x-next', 'router') 538 | .expect(404, done) 539 | }) 540 | }) 541 | 542 | describe('promise support', function () { 543 | it('should pass rejected promise value', function (done) { 544 | const router = new Router() 545 | const route = router.route('/foo') 546 | const server = createServer(router) 547 | 548 | route.all(function createError (req, res, next) { 549 | return Promise.reject(new Error('boom!')) 550 | }) 551 | 552 | route.all(helloWorld) 553 | 554 | route.all(function handleError (err, req, res, next) { 555 | res.statusCode = 500 556 | res.end('caught: ' + err.message) 557 | }) 558 | 559 | request(server) 560 | .get('/foo') 561 | .expect(500, 'caught: boom!', done) 562 | }) 563 | 564 | it('should pass rejected promise without value', function (done) { 565 | const router = new Router() 566 | const route = router.route('/foo') 567 | const server = createServer(router) 568 | 569 | route.all(function createError (req, res, next) { 570 | return Promise.reject() // eslint-disable-line prefer-promise-reject-errors 571 | }) 572 | 573 | route.all(helloWorld) 574 | 575 | route.all(function handleError (err, req, res, next) { 576 | res.statusCode = 500 577 | res.end('caught: ' + err.message) 578 | }) 579 | 580 | request(server) 581 | .get('/foo') 582 | .expect(500, 'caught: Rejected promise', done) 583 | }) 584 | 585 | it('should ignore resolved promise', function (done) { 586 | const router = new Router() 587 | const route = router.route('/foo') 588 | const server = createServer(router) 589 | 590 | route.all(function createError (req, res, next) { 591 | saw(req, res) 592 | return Promise.resolve('foo') 593 | }) 594 | 595 | route.all(function () { 596 | done(new Error('Unexpected route invoke')) 597 | }) 598 | 599 | request(server) 600 | .get('/foo') 601 | .expect(200, 'saw GET /foo', done) 602 | }) 603 | 604 | describe('error handling', function () { 605 | it('should pass rejected promise value', function (done) { 606 | const router = new Router() 607 | const route = router.route('/foo') 608 | const server = createServer(router) 609 | 610 | route.all(function createError (req, res, next) { 611 | return Promise.reject(new Error('boom!')) 612 | }) 613 | 614 | route.all(function handleError (err, req, res, next) { 615 | return Promise.reject(new Error('caught: ' + err.message)) 616 | }) 617 | 618 | route.all(function handleError (err, req, res, next) { 619 | res.statusCode = 500 620 | res.end('caught again: ' + err.message) 621 | }) 622 | 623 | request(server) 624 | .get('/foo') 625 | .expect(500, 'caught again: caught: boom!', done) 626 | }) 627 | 628 | it('should pass rejected promise without value', function (done) { 629 | const router = new Router() 630 | const route = router.route('/foo') 631 | const server = createServer(router) 632 | 633 | route.all(function createError (req, res, next) { 634 | return Promise.reject(new Error('boom!')) 635 | }) 636 | 637 | route.all(function handleError (err, req, res, next) { 638 | assert.equal(err.message, 'boom!') 639 | return Promise.reject() // eslint-disable-line prefer-promise-reject-errors 640 | }) 641 | 642 | route.all(function handleError (err, req, res, next) { 643 | res.statusCode = 500 644 | res.end('caught again: ' + err.message) 645 | }) 646 | 647 | request(server) 648 | .get('/foo') 649 | .expect(500, 'caught again: Rejected promise', done) 650 | }) 651 | 652 | it('should ignore resolved promise', function (done) { 653 | const router = new Router() 654 | const route = router.route('/foo') 655 | const server = createServer(router) 656 | 657 | route.all(function createError (req, res, next) { 658 | return Promise.reject(new Error('boom!')) 659 | }) 660 | 661 | route.all(function handleError (err, req, res, next) { 662 | res.statusCode = 500 663 | res.end('caught: ' + err.message) 664 | return Promise.resolve('foo') 665 | }) 666 | 667 | route.all(function () { 668 | done(new Error('Unexpected route invoke')) 669 | }) 670 | 671 | request(server) 672 | .get('/foo') 673 | .expect(500, 'caught: boom!', done) 674 | }) 675 | }) 676 | }) 677 | 678 | describe('path', function () { 679 | describe('using ":name"', function () { 680 | it('should name a capture group', function (done) { 681 | const router = new Router() 682 | const route = router.route('/:foo') 683 | const server = createServer(router) 684 | 685 | route.all(sendParams) 686 | 687 | request(server) 688 | .get('/bar') 689 | .expect(200, { foo: 'bar' }, done) 690 | }) 691 | 692 | it('should match single path segment', function (done) { 693 | const router = new Router() 694 | const route = router.route('/:foo') 695 | const server = createServer(router) 696 | 697 | route.all(sendParams) 698 | 699 | request(server) 700 | .get('/bar/bar') 701 | .expect(404, done) 702 | }) 703 | 704 | it('should work multiple times', function (done) { 705 | const router = new Router() 706 | const route = router.route('/:foo/:bar') 707 | const server = createServer(router) 708 | 709 | route.all(sendParams) 710 | 711 | request(server) 712 | .get('/fizz/buzz') 713 | .expect(200, { foo: 'fizz', bar: 'buzz' }, done) 714 | }) 715 | 716 | it('should work inside literal parentheses', function (done) { 717 | const router = new Router() 718 | const route = router.route('/:user\\(:op\\)') 719 | const server = createServer(router) 720 | 721 | route.all(sendParams) 722 | 723 | request(server) 724 | .get('/tj(edit)') 725 | .expect(200, { user: 'tj', op: 'edit' }, done) 726 | }) 727 | 728 | it('should work within arrays', function (done) { 729 | const router = new Router() 730 | const route = router.route(['/user/:user/poke', '/user/:user/pokes']) 731 | const server = createServer(router) 732 | 733 | route.all(sendParams) 734 | series([ 735 | function (cb) { 736 | request(server) 737 | .get('/user/tj/poke') 738 | .expect(200, { user: 'tj' }, cb) 739 | }, 740 | function (cb) { 741 | request(server) 742 | .get('/user/tj/pokes') 743 | .expect(200, { user: 'tj' }, cb) 744 | } 745 | ], done) 746 | }) 747 | }) 748 | 749 | describe('using "{:name}"', function () { 750 | it('should name an optional parameter', function (done) { 751 | const router = new Router() 752 | const route = router.route('{/:foo}') 753 | const server = createServer(router) 754 | 755 | route.all(sendParams) 756 | series([ 757 | function (cb) { 758 | request(server) 759 | .get('/bar') 760 | .expect(200, { foo: 'bar' }, cb) 761 | }, 762 | function (cb) { 763 | request(server) 764 | .get('/') 765 | .expect(200, {}, cb) 766 | } 767 | ], done) 768 | }) 769 | 770 | it('should work in any segment', function (done) { 771 | const router = new Router() 772 | const route = router.route('/user{/:foo}/delete') 773 | const server = createServer(router) 774 | 775 | route.all(sendParams) 776 | series([ 777 | function (cb) { 778 | request(server) 779 | .get('/user/bar/delete') 780 | .expect(200, { foo: 'bar' }, cb) 781 | }, 782 | function (cb) { 783 | request(server) 784 | .get('/user/delete') 785 | .expect(200, {}, cb) 786 | } 787 | ], done) 788 | }) 789 | }) 790 | 791 | describe('using "*name"', function () { 792 | it('should name a zero-or-more repeated parameter', function (done) { 793 | const router = new Router() 794 | const route = router.route('{/*foo}') 795 | const server = createServer(router) 796 | 797 | route.all(sendParams) 798 | series([ 799 | function (cb) { 800 | request(server) 801 | .get('/') 802 | .expect(200, {}, cb) 803 | }, 804 | function (cb) { 805 | request(server) 806 | .get('/bar') 807 | .expect(200, { foo: ['bar'] }, cb) 808 | }, 809 | function (cb) { 810 | request(server) 811 | .get('/fizz/buzz') 812 | .expect(200, { foo: ['fizz', 'buzz'] }, cb) 813 | } 814 | ], done) 815 | }) 816 | 817 | it('should work in any segment', function (done) { 818 | const router = new Router() 819 | const route = router.route('/user{/*foo}/delete') 820 | const server = createServer(router) 821 | 822 | route.all(sendParams) 823 | series([ 824 | function (cb) { 825 | request(server) 826 | .get('/user/delete') 827 | .expect(200, {}, cb) 828 | }, 829 | function (cb) { 830 | request(server) 831 | .get('/user/bar/delete') 832 | .expect(200, { foo: ['bar'] }, cb) 833 | }, 834 | function (cb) { 835 | request(server) 836 | .get('/user/fizz/buzz/delete') 837 | .expect(200, { foo: ['fizz', 'buzz'] }, cb) 838 | } 839 | ], done) 840 | }) 841 | }) 842 | 843 | describe('using regular expression with param name "(?pattern)"', function () { 844 | it('should limit capture group to regexp match', function (done) { 845 | const router = new Router() 846 | const route = router.route(/\/(?[0-9]+)/) 847 | const server = createServer(router) 848 | 849 | route.all(sendParams) 850 | 851 | series([ 852 | function (cb) { 853 | request(server) 854 | .get('/foo') 855 | .expect(404, cb) 856 | }, 857 | function (cb) { 858 | request(server) 859 | .get('/42') 860 | .expect(200, { foo: '42' }, cb) 861 | } 862 | ], done) 863 | }) 864 | }) 865 | 866 | describe('using "(regexp)"', function () { 867 | it('should add capture group using regexp', function (done) { 868 | const router = new Router() 869 | const route = router.route(/\/page_([0-9]+)/) 870 | const server = createServer(router) 871 | 872 | route.all(sendParams) 873 | series([ 874 | function (cb) { 875 | request(server) 876 | .get('/page_foo') 877 | .expect(404, cb) 878 | }, 879 | function (cb) { 880 | request(server) 881 | .get('/page_42') 882 | .expect(200, { 0: '42' }, cb) 883 | } 884 | ], done) 885 | }) 886 | 887 | it('should not treat regexp as literal regexp', function () { 888 | const router = new Router() 889 | assert.throws(function () { 890 | router.route('/([a-z]+:n[0-9]+)') 891 | }, /TypeError: Unexpected \( at/) 892 | }) 893 | }) 894 | }) 895 | }) 896 | }) 897 | 898 | function helloWorld (req, res) { 899 | res.statusCode = 200 900 | res.setHeader('Content-Type', 'text/plain') 901 | res.end('hello, world') 902 | } 903 | 904 | function saw (req, res) { 905 | const msg = 'saw ' + req.method + ' ' + req.url 906 | res.statusCode = 200 907 | res.setHeader('Content-Type', 'text/plain') 908 | res.end(msg) 909 | } 910 | 911 | function sendParams (req, res) { 912 | res.statusCode = 200 913 | res.setHeader('Content-Type', 'application/json') 914 | res.end(JSON.stringify(req.params)) 915 | } 916 | -------------------------------------------------------------------------------- /test/router.js: -------------------------------------------------------------------------------- 1 | const { it, describe } = require('mocha') 2 | const series = require('run-series') 3 | const Router = require('..') 4 | const utils = require('./support/utils') 5 | 6 | const assert = utils.assert 7 | const createHitHandle = utils.createHitHandle 8 | const createServer = utils.createServer 9 | const rawrequest = utils.rawrequest 10 | const request = utils.request 11 | const shouldHaveBody = utils.shouldHaveBody 12 | const shouldHitHandle = utils.shouldHitHandle 13 | const shouldNotHaveBody = utils.shouldNotHaveBody 14 | const shouldNotHitHandle = utils.shouldNotHitHandle 15 | const methods = utils.methods 16 | 17 | describe('Router', function () { 18 | it('should return a function', function () { 19 | assert.equal(typeof Router(), 'function') 20 | }) 21 | 22 | it('should return a function using new', function () { 23 | assert.equal(typeof (new Router()), 'function') 24 | }) 25 | 26 | it('should reject missing callback', function () { 27 | const router = new Router() 28 | assert.throws(function () { router({}, {}) }, /argument callback is required/) 29 | }) 30 | 31 | it('should invoke callback without "req.url"', function (done) { 32 | const router = new Router() 33 | router.use(saw) 34 | router({}, {}, done) 35 | }) 36 | 37 | describe('.all(path, fn)', function () { 38 | it('should be chainable', function () { 39 | const router = new Router() 40 | assert.equal(router.all('/', helloWorld), router) 41 | }) 42 | 43 | it('should respond to all methods', function (done) { 44 | const router = new Router() 45 | const server = createServer(router) 46 | router.all('/', helloWorld) 47 | 48 | series(methods.map(function (method) { 49 | return function (cb) { 50 | if (method === 'connect') { 51 | // CONNECT is tricky and supertest doesn't support it 52 | return cb() 53 | } 54 | if (method === 'query' && process.version.startsWith('v21')) { 55 | return cb() 56 | } 57 | 58 | const body = method !== 'head' 59 | ? shouldHaveBody(Buffer.from('hello, world')) 60 | : shouldNotHaveBody() 61 | 62 | request(server)[method]('/') 63 | .expect(200) 64 | .expect(body) 65 | .end(cb) 66 | } 67 | }), done) 68 | }) 69 | 70 | it('should support array of paths', function (done) { 71 | const router = new Router() 72 | const server = createServer(router) 73 | 74 | router.all(['/foo', '/bar'], saw) 75 | series([ 76 | function (cb) { 77 | request(server) 78 | .get('/') 79 | .expect(404, cb) 80 | }, 81 | function (cb) { 82 | request(server) 83 | .get('/foo') 84 | .expect(200, 'saw GET /foo', cb) 85 | }, 86 | function (cb) { 87 | request(server) 88 | .get('/bar') 89 | .expect(200, 'saw GET /bar', cb) 90 | } 91 | ], done) 92 | }) 93 | 94 | it('should support regexp path', function (done) { 95 | const router = new Router() 96 | const server = createServer(router) 97 | 98 | router.all(/^\/[a-z]oo$/, saw) 99 | series([ 100 | function (cb) { 101 | request(server) 102 | .get('/') 103 | .expect(404, cb) 104 | }, 105 | function (cb) { 106 | request(server) 107 | .get('/foo') 108 | .expect(200, 'saw GET /foo', cb) 109 | }, 110 | function (cb) { 111 | request(server) 112 | .get('/zoo') 113 | .expect(200, 'saw GET /zoo', cb) 114 | } 115 | ], done) 116 | }) 117 | 118 | it('should support parameterized path', function (done) { 119 | const router = new Router() 120 | const server = createServer(router) 121 | 122 | router.all('/:thing', saw) 123 | series([ 124 | function (cb) { 125 | request(server) 126 | .get('/') 127 | .expect(404, cb) 128 | }, 129 | function (cb) { 130 | request(server) 131 | .get('/foo') 132 | .expect(200, 'saw GET /foo', cb) 133 | }, 134 | function (cb) { 135 | request(server) 136 | .get('/bar') 137 | .expect(200, 'saw GET /bar', cb) 138 | }, 139 | function (cb) { 140 | request(server) 141 | .get('/foo/bar') 142 | .expect(404, cb) 143 | } 144 | ], done) 145 | }) 146 | 147 | it('should not stack overflow with many registered routes', function (done) { 148 | this.timeout(5000) // long-running test 149 | 150 | const router = new Router() 151 | const server = createServer(router) 152 | 153 | for (let i = 0; i < 6000; i++) { 154 | router.get('/thing' + i, helloWorld) 155 | } 156 | 157 | router.get('/', helloWorld) 158 | 159 | request(server) 160 | .get('/') 161 | .expect(200, 'hello, world', done) 162 | }) 163 | 164 | it('should not stack overflow with a large sync stack', function (done) { 165 | this.timeout(5000) // long-running test 166 | 167 | const router = new Router() 168 | const server = createServer(router) 169 | 170 | for (let i = 0; i < 6000; i++) { 171 | router.get('/foo', function (req, res, next) { next() }) 172 | } 173 | 174 | router.get('/foo', helloWorld) 175 | 176 | request(server) 177 | .get('/foo') 178 | .expect(200, 'hello, world', done) 179 | }) 180 | 181 | describe('with "caseSensitive" option', function () { 182 | it('should not match paths case-sensitively by default', function (done) { 183 | const router = new Router() 184 | const server = createServer(router) 185 | 186 | router.all('/foo/bar', saw) 187 | series([ 188 | function (cb) { 189 | request(server) 190 | .get('/foo/bar') 191 | .expect(200, 'saw GET /foo/bar', cb) 192 | }, 193 | function (cb) { 194 | request(server) 195 | .get('/FOO/bar') 196 | .expect(200, 'saw GET /FOO/bar', cb) 197 | }, 198 | function (cb) { 199 | request(server) 200 | .get('/FOO/BAR') 201 | .expect(200, 'saw GET /FOO/BAR', cb) 202 | } 203 | ], done) 204 | }) 205 | 206 | it('should not match paths case-sensitively when false', function (done) { 207 | const router = new Router({ caseSensitive: false }) 208 | const server = createServer(router) 209 | 210 | router.all('/foo/bar', saw) 211 | series([ 212 | function (cb) { 213 | request(server) 214 | .get('/foo/bar') 215 | .expect(200, 'saw GET /foo/bar', cb) 216 | }, 217 | function (cb) { 218 | request(server) 219 | .get('/FOO/bar') 220 | .expect(200, 'saw GET /FOO/bar', cb) 221 | }, 222 | function (cb) { 223 | request(server) 224 | .get('/FOO/BAR') 225 | .expect(200, 'saw GET /FOO/BAR', cb) 226 | } 227 | ], done) 228 | }) 229 | 230 | it('should match paths case-sensitively when true', function (done) { 231 | const router = new Router({ caseSensitive: true }) 232 | const server = createServer(router) 233 | 234 | router.all('/foo/bar', saw) 235 | series([ 236 | function (cb) { 237 | request(server) 238 | .get('/foo/bar') 239 | .expect(200, 'saw GET /foo/bar', cb) 240 | }, 241 | function (cb) { 242 | request(server) 243 | .get('/FOO/bar') 244 | .expect(404, cb) 245 | }, 246 | function (cb) { 247 | request(server) 248 | .get('/FOO/BAR') 249 | .expect(404, cb) 250 | } 251 | ], done) 252 | }) 253 | }) 254 | 255 | describe('with "strict" option', function () { 256 | it('should accept optional trailing slashes by default', function (done) { 257 | const router = new Router() 258 | const server = createServer(router) 259 | 260 | router.all('/foo', saw) 261 | series([ 262 | function (cb) { 263 | request(server) 264 | .get('/foo') 265 | .expect(200, 'saw GET /foo', cb) 266 | }, 267 | function (cb) { 268 | request(server) 269 | .get('/foo/') 270 | .expect(200, 'saw GET /foo/', cb) 271 | } 272 | ], done) 273 | }) 274 | 275 | it('should accept optional trailing slashes when false', function (done) { 276 | const router = new Router({ strict: false }) 277 | const server = createServer(router) 278 | 279 | router.all('/foo', saw) 280 | series([ 281 | function (cb) { 282 | request(server) 283 | .get('/foo') 284 | .expect(200, 'saw GET /foo', cb) 285 | }, 286 | function (cb) { 287 | request(server) 288 | .get('/foo/') 289 | .expect(200, 'saw GET /foo/', cb) 290 | } 291 | ], done) 292 | }) 293 | 294 | it('should not accept optional trailing slashes when true', function (done) { 295 | const router = new Router({ strict: true }) 296 | const server = createServer(router) 297 | 298 | router.all('/foo', saw) 299 | series([ 300 | function (cb) { 301 | request(server) 302 | .get('/foo') 303 | .expect(200, 'saw GET /foo', cb) 304 | }, 305 | function (cb) { 306 | request(server) 307 | .get('/foo/') 308 | .expect(404, cb) 309 | } 310 | ], done) 311 | }) 312 | }) 313 | }) 314 | 315 | methods.slice().sort().forEach(function (method) { 316 | if (method === 'connect') { 317 | // CONNECT is tricky and supertest doesn't support it 318 | return 319 | } 320 | if (method === 'query' && process.version.startsWith('v21')) { 321 | return 322 | } 323 | 324 | const body = method !== 'head' 325 | ? shouldHaveBody(Buffer.from('hello, world')) 326 | : shouldNotHaveBody() 327 | 328 | describe('.' + method + '(path, ...fn)', function () { 329 | it('should be chainable', function () { 330 | const router = new Router() 331 | assert.equal(router[method]('/', helloWorld), router) 332 | }) 333 | 334 | it('should respond to a ' + method.toUpperCase() + ' request', function (done) { 335 | const router = new Router() 336 | const server = createServer(router) 337 | 338 | router[method]('/', helloWorld) 339 | 340 | request(server)[method]('/') 341 | .expect(200) 342 | .expect(body) 343 | .end(done) 344 | }) 345 | 346 | it('should reject invalid fn', function () { 347 | const router = new Router() 348 | assert.throws(router[method].bind(router, '/', 2), /argument handler must be a function/) 349 | }) 350 | 351 | it('should support array of paths', function (done) { 352 | const router = new Router() 353 | const server = createServer(router) 354 | 355 | router[method](['/foo', '/bar'], createHitHandle(1), helloWorld) 356 | 357 | series([ 358 | function (cb) { 359 | request(server)[method]('/') 360 | .expect(404) 361 | .expect(shouldNotHitHandle(1)) 362 | .end(cb) 363 | }, 364 | function (cb) { 365 | request(server)[method]('/foo') 366 | .expect(200) 367 | .expect(shouldHitHandle(1)) 368 | .expect(body) 369 | .end(cb) 370 | }, 371 | function (cb) { 372 | request(server)[method]('/bar') 373 | .expect(200) 374 | .expect(shouldHitHandle(1)) 375 | .expect(body) 376 | .end(cb) 377 | } 378 | ], done) 379 | }) 380 | 381 | it('should support parameterized path', function (done) { 382 | const router = new Router() 383 | const server = createServer(router) 384 | 385 | router[method]('/:thing', createHitHandle(1), helloWorld) 386 | 387 | series([ 388 | function (cb) { 389 | request(server)[method]('/') 390 | .expect(404) 391 | .expect(shouldNotHitHandle(1)) 392 | .end(cb) 393 | }, 394 | function (cb) { 395 | request(server)[method]('/foo') 396 | .expect(200) 397 | .expect(shouldHitHandle(1)) 398 | .expect(body) 399 | .end(cb) 400 | }, 401 | function (cb) { 402 | request(server)[method]('/bar') 403 | .expect(200) 404 | .expect(shouldHitHandle(1)) 405 | .expect(body) 406 | .end(cb) 407 | }, 408 | function (cb) { 409 | request(server)[method]('/foo/bar') 410 | .expect(404) 411 | .expect(shouldNotHitHandle(1)) 412 | .end(cb) 413 | } 414 | ], done) 415 | }) 416 | 417 | it('should accept multiple arguments', function (done) { 418 | const router = new Router() 419 | const server = createServer(router) 420 | 421 | router[method]('/', createHitHandle(1), createHitHandle(2), helloWorld) 422 | 423 | request(server)[method]('/') 424 | .expect(200) 425 | .expect(shouldHitHandle(1)) 426 | .expect(shouldHitHandle(2)) 427 | .expect(body) 428 | .end(done) 429 | }) 430 | 431 | describe('req.baseUrl', function () { 432 | it('should be empty', function (done) { 433 | const router = new Router() 434 | const server = createServer(router) 435 | 436 | router[method]('/foo', function handle (req, res) { 437 | res.setHeader('x-url-base', JSON.stringify(req.baseUrl)) 438 | res.end() 439 | }) 440 | 441 | request(server)[method]('/foo') 442 | .expect('x-url-base', '""') 443 | .expect(200, done) 444 | }) 445 | }) 446 | 447 | describe('req.route', function () { 448 | it('should be a Route', function (done) { 449 | const router = new Router() 450 | const server = createServer(router) 451 | 452 | router[method]('/foo', function handle (req, res) { 453 | res.setHeader('x-is-route', String(req.route instanceof Router.Route)) 454 | res.end() 455 | }) 456 | 457 | request(server)[method]('/foo') 458 | .expect('x-is-route', 'true') 459 | .expect(200, done) 460 | }) 461 | 462 | it('should be the matched route', function (done) { 463 | const router = new Router() 464 | const server = createServer(router) 465 | 466 | router[method]('/foo', function handle (req, res) { 467 | res.setHeader('x-is-route', String(req.route.path === '/foo')) 468 | res.end() 469 | }) 470 | 471 | request(server)[method]('/foo') 472 | .expect('x-is-route', 'true') 473 | .expect(200, done) 474 | }) 475 | }) 476 | }) 477 | }) 478 | 479 | describe('.use(...fn)', function () { 480 | it('should reject missing functions', function () { 481 | const router = new Router() 482 | assert.throws(router.use.bind(router), /argument handler is required/) 483 | }) 484 | 485 | it('should reject empty array', function () { 486 | const router = new Router() 487 | assert.throws(router.use.bind(router, []), /argument handler is required/) 488 | }) 489 | 490 | it('should reject non-functions', function () { 491 | const router = new Router() 492 | assert.throws(router.use.bind(router, '/', 'hello'), /argument handler must be a function/) 493 | assert.throws(router.use.bind(router, '/', 5), /argument handler must be a function/) 494 | assert.throws(router.use.bind(router, '/', null), /argument handler must be a function/) 495 | assert.throws(router.use.bind(router, '/', new Date()), /argument handler must be a function/) 496 | }) 497 | 498 | it('should be chainable', function () { 499 | const router = new Router() 500 | assert.equal(router.use(helloWorld), router) 501 | }) 502 | 503 | it('should invoke function for all requests', function (done) { 504 | const router = new Router() 505 | const server = createServer(router) 506 | 507 | router.use(saw) 508 | 509 | series([ 510 | function (cb) { 511 | request(server) 512 | .get('/') 513 | .expect(200, 'saw GET /', cb) 514 | }, 515 | function (cb) { 516 | request(server) 517 | .put('/') 518 | .expect(200, 'saw PUT /', cb) 519 | }, 520 | function (cb) { 521 | request(server) 522 | .post('/foo') 523 | .expect(200, 'saw POST /foo', cb) 524 | }, 525 | function (cb) { 526 | rawrequest(server) 527 | .options('*') 528 | .expect(200, 'saw OPTIONS *', cb) 529 | } 530 | ], done) 531 | }) 532 | 533 | it('should not invoke for blank URLs', function (done) { 534 | const router = new Router() 535 | const server = createServer(function hander (req, res, next) { 536 | req.url = '' 537 | router(req, res, next) 538 | }) 539 | 540 | router.use(saw) 541 | 542 | request(server) 543 | .get('/') 544 | .expect(404, done) 545 | }) 546 | 547 | it('should support another router', function (done) { 548 | const inner = new Router() 549 | const router = new Router() 550 | const server = createServer(router) 551 | 552 | inner.use(saw) 553 | router.use(inner) 554 | 555 | request(server) 556 | .get('/') 557 | .expect(200, 'saw GET /', done) 558 | }) 559 | 560 | it('should accept multiple arguments', function (done) { 561 | const router = new Router() 562 | const server = createServer(router) 563 | 564 | router.use(createHitHandle(1), createHitHandle(2), helloWorld) 565 | 566 | request(server) 567 | .get('/') 568 | .expect(shouldHitHandle(1)) 569 | .expect(shouldHitHandle(2)) 570 | .expect(200, 'hello, world', done) 571 | }) 572 | 573 | it('should accept single array of middleware', function (done) { 574 | const router = new Router() 575 | const server = createServer(router) 576 | 577 | router.use([createHitHandle(1), createHitHandle(2), helloWorld]) 578 | 579 | request(server) 580 | .get('/') 581 | .expect(shouldHitHandle(1)) 582 | .expect(shouldHitHandle(2)) 583 | .expect(200, 'hello, world', done) 584 | }) 585 | 586 | it('should accept nested arrays of middleware', function (done) { 587 | const router = new Router() 588 | const server = createServer(router) 589 | 590 | router.use([[createHitHandle(1), createHitHandle(2)], createHitHandle(3)], helloWorld) 591 | 592 | request(server) 593 | .get('/') 594 | .expect(shouldHitHandle(1)) 595 | .expect(shouldHitHandle(2)) 596 | .expect(shouldHitHandle(3)) 597 | .expect(200, 'hello, world', done) 598 | }) 599 | 600 | it('should not invoke singular error function', function (done) { 601 | const router = new Router() 602 | const server = createServer(router) 603 | 604 | router.use(function handleError (err, req, res, next) { 605 | throw err || new Error('boom!') 606 | }) 607 | 608 | request(server) 609 | .get('/') 610 | .expect(404, done) 611 | }) 612 | 613 | it('should not stack overflow with a large sync stack', function (done) { 614 | this.timeout(5000) // long-running test 615 | 616 | const router = new Router() 617 | const server = createServer(router) 618 | 619 | for (let i = 0; i < 6000; i++) { 620 | router.use(function (req, res, next) { next() }) 621 | } 622 | 623 | router.use(helloWorld) 624 | 625 | request(server) 626 | .get('/') 627 | .expect(200, 'hello, world', done) 628 | }) 629 | 630 | describe('error handling', function () { 631 | it('should invoke error function after next(err)', function (done) { 632 | const router = new Router() 633 | const server = createServer(router) 634 | 635 | router.use(function handle (req, res, next) { 636 | next(new Error('boom!')) 637 | }) 638 | 639 | router.use(sawError) 640 | 641 | request(server) 642 | .get('/') 643 | .expect(200, 'saw Error: boom!', done) 644 | }) 645 | 646 | it('should invoke error function after throw err', function (done) { 647 | const router = new Router() 648 | const server = createServer(router) 649 | 650 | router.use(function handle (req, res, next) { 651 | throw new Error('boom!') 652 | }) 653 | 654 | router.use(sawError) 655 | 656 | request(server) 657 | .get('/') 658 | .expect(200, 'saw Error: boom!', done) 659 | }) 660 | 661 | it('should not invoke error functions above function', function (done) { 662 | const router = new Router() 663 | const server = createServer(router) 664 | 665 | router.use(sawError) 666 | 667 | router.use(function handle (req, res, next) { 668 | throw new Error('boom!') 669 | }) 670 | 671 | request(server) 672 | .get('/') 673 | .expect(500, done) 674 | }) 675 | }) 676 | 677 | describe('next("route")', function () { 678 | it('should invoke next handler', function (done) { 679 | const router = new Router() 680 | const server = createServer(router) 681 | 682 | router.use(function handle (req, res, next) { 683 | res.setHeader('x-next', 'route') 684 | next('route') 685 | }) 686 | 687 | router.use(saw) 688 | 689 | request(server) 690 | .get('/') 691 | .expect('x-next', 'route') 692 | .expect(200, 'saw GET /', done) 693 | }) 694 | 695 | it('should invoke next function', function (done) { 696 | const router = new Router() 697 | const server = createServer(router) 698 | 699 | function goNext (req, res, next) { 700 | res.setHeader('x-next', 'route') 701 | next('route') 702 | } 703 | 704 | router.use(createHitHandle(1), goNext, createHitHandle(2), saw) 705 | 706 | request(server) 707 | .get('/') 708 | .expect(shouldHitHandle(1)) 709 | .expect('x-next', 'route') 710 | .expect(shouldHitHandle(2)) 711 | .expect(200, 'saw GET /', done) 712 | }) 713 | 714 | it('should not invoke error handlers', function (done) { 715 | const router = new Router() 716 | const server = createServer(router) 717 | 718 | router.use(function handle (req, res, next) { 719 | res.setHeader('x-next', 'route') 720 | next('route') 721 | }) 722 | 723 | router.use(sawError) 724 | 725 | request(server) 726 | .get('/') 727 | .expect('x-next', 'route') 728 | .expect(404, done) 729 | }) 730 | }) 731 | 732 | describe('next("router")', function () { 733 | it('should exit the router', function (done) { 734 | const router = new Router() 735 | const server = createServer(router) 736 | 737 | function handle (req, res, next) { 738 | res.setHeader('x-next', 'router') 739 | next('router') 740 | } 741 | 742 | router.use(handle, createHitHandle(1)) 743 | router.use(saw) 744 | 745 | request(server) 746 | .get('/') 747 | .expect('x-next', 'router') 748 | .expect(shouldNotHitHandle(1)) 749 | .expect(404, done) 750 | }) 751 | 752 | it('should not invoke error handlers', function (done) { 753 | const router = new Router() 754 | const server = createServer(router) 755 | 756 | router.use(function handle (req, res, next) { 757 | res.setHeader('x-next', 'router') 758 | next('route') 759 | }) 760 | 761 | router.use(sawError) 762 | 763 | request(server) 764 | .get('/') 765 | .expect('x-next', 'router') 766 | .expect(404, done) 767 | }) 768 | }) 769 | 770 | describe('promise support', function () { 771 | it('should pass rejected promise value', function (done) { 772 | const router = new Router() 773 | const server = createServer(router) 774 | 775 | router.use(function createError (req, res, next) { 776 | return Promise.reject(new Error('boom!')) 777 | }) 778 | 779 | router.use(sawError) 780 | 781 | request(server) 782 | .get('/') 783 | .expect(200, 'saw Error: boom!', done) 784 | }) 785 | 786 | it('should pass rejected promise without value', function (done) { 787 | const router = new Router() 788 | const server = createServer(router) 789 | 790 | router.use(function createError (req, res, next) { 791 | return Promise.reject() // eslint-disable-line prefer-promise-reject-errors 792 | }) 793 | 794 | router.use(sawError) 795 | 796 | request(server) 797 | .get('/') 798 | .expect(200, 'saw Error: Rejected promise', done) 799 | }) 800 | 801 | it('should ignore resolved promise', function (done) { 802 | const router = new Router() 803 | const server = createServer(router) 804 | 805 | router.use(function createError (req, res, next) { 806 | saw(req, res) 807 | return Promise.resolve('foo') 808 | }) 809 | 810 | router.use(function () { 811 | done(new Error('Unexpected middleware invoke')) 812 | }) 813 | 814 | request(server) 815 | .get('/foo') 816 | .expect(200, 'saw GET /foo', done) 817 | }) 818 | 819 | describe('error handling', function () { 820 | it('should pass rejected promise value', function (done) { 821 | const router = new Router() 822 | const server = createServer(router) 823 | 824 | router.use(function createError (req, res, next) { 825 | return Promise.reject(new Error('boom!')) 826 | }) 827 | 828 | router.use(function handleError (err, req, res, next) { 829 | return Promise.reject(new Error('caught: ' + err.message)) 830 | }) 831 | 832 | router.use(sawError) 833 | 834 | request(server) 835 | .get('/') 836 | .expect(200, 'saw Error: caught: boom!', done) 837 | }) 838 | 839 | it('should pass rejected promise without value', function (done) { 840 | const router = new Router() 841 | const server = createServer(router) 842 | 843 | router.use(function createError (req, res, next) { 844 | return Promise.reject() // eslint-disable-line prefer-promise-reject-errors 845 | }) 846 | 847 | router.use(function handleError (err, req, res, next) { 848 | return Promise.reject(new Error('caught: ' + err.message)) 849 | }) 850 | 851 | router.use(sawError) 852 | 853 | request(server) 854 | .get('/') 855 | .expect(200, 'saw Error: caught: Rejected promise', done) 856 | }) 857 | 858 | it('should ignore resolved promise', function (done) { 859 | const router = new Router() 860 | const server = createServer(router) 861 | 862 | router.use(function createError (req, res, next) { 863 | return Promise.reject(new Error('boom!')) 864 | }) 865 | 866 | router.use(function handleError (err, req, res, next) { 867 | sawError(err, req, res, next) 868 | return Promise.resolve('foo') 869 | }) 870 | 871 | router.use(function () { 872 | done(new Error('Unexpected middleware invoke')) 873 | }) 874 | 875 | request(server) 876 | .get('/foo') 877 | .expect(200, 'saw Error: boom!', done) 878 | }) 879 | }) 880 | }) 881 | 882 | describe('req.baseUrl', function () { 883 | it('should be empty', function (done) { 884 | const router = new Router() 885 | const server = createServer(router) 886 | 887 | router.use(sawBase) 888 | 889 | request(server) 890 | .get('/foo/bar') 891 | .expect(200, 'saw ', done) 892 | }) 893 | }) 894 | }) 895 | 896 | describe('.use(path, ...fn)', function () { 897 | it('should be chainable', function () { 898 | const router = new Router() 899 | assert.equal(router.use('/', helloWorld), router) 900 | }) 901 | 902 | it('should invoke when req.url starts with path', function (done) { 903 | const router = new Router() 904 | const server = createServer(router) 905 | 906 | router.use('/foo', saw) 907 | series([ 908 | function (cb) { 909 | request(server) 910 | .get('/') 911 | .expect(404, cb) 912 | }, 913 | function (cb) { 914 | request(server) 915 | .post('/foo') 916 | .expect(200, 'saw POST /', cb) 917 | }, 918 | function (cb) { 919 | request(server) 920 | .post('/foo/bar') 921 | .expect(200, 'saw POST /bar', cb) 922 | } 923 | ], done) 924 | }) 925 | 926 | it('should match if path has trailing slash', function (done) { 927 | const router = new Router() 928 | const server = createServer(router) 929 | 930 | router.use('/foo/', saw) 931 | 932 | series([ 933 | function (cb) { 934 | request(server) 935 | .get('/') 936 | .expect(404, cb) 937 | }, 938 | function (cb) { 939 | request(server) 940 | .post('/foo') 941 | .expect(200, 'saw POST /', cb) 942 | }, 943 | function (cb) { 944 | request(server) 945 | .post('/foo/bar') 946 | .expect(200, 'saw POST /bar', cb) 947 | } 948 | ], done) 949 | }) 950 | 951 | it('should support array of paths', function (done) { 952 | const router = new Router() 953 | const server = createServer(router) 954 | 955 | router.use(['/foo/', '/bar'], saw) 956 | 957 | series([ 958 | function (cb) { 959 | request(server) 960 | .get('/') 961 | .expect(404, cb) 962 | }, 963 | function (cb) { 964 | request(server) 965 | .get('/foo') 966 | .expect(200, 'saw GET /', cb) 967 | }, 968 | function (cb) { 969 | request(server) 970 | .get('/bar') 971 | .expect(200, 'saw GET /', cb) 972 | } 973 | ], done) 974 | }) 975 | 976 | it('should support regexp path', function (done) { 977 | const router = new Router() 978 | const server = createServer(router) 979 | 980 | router.use(/^\/[a-z]oo$/, saw) 981 | series([ 982 | function (cb) { 983 | request(server) 984 | .get('/') 985 | .expect(404, cb) 986 | }, 987 | function (cb) { 988 | request(server) 989 | .get('/foo') 990 | .expect(200, 'saw GET /', cb) 991 | }, 992 | function (cb) { 993 | request(server) 994 | .get('/fooo') 995 | .expect(404, cb) 996 | }, 997 | function (cb) { 998 | request(server) 999 | .get('/zoo/bear') 1000 | .expect(404, cb) 1001 | }, 1002 | function (cb) { 1003 | request(server) 1004 | .get('/get/zoo') 1005 | .expect(404, cb) 1006 | } 1007 | ], done) 1008 | }) 1009 | 1010 | it('should support regexp path with params', function (done) { 1011 | const router = new Router() 1012 | const server = createServer(router) 1013 | 1014 | router.use(/^\/([a-z]oo)$/, function (req, res, next) { 1015 | createHitHandle(req.params[0])(req, res, next) 1016 | }, saw) 1017 | 1018 | router.use(/^\/([a-z]oo)\/(?bear)$/, function (req, res, next) { 1019 | createHitHandle(req.params[0] + req.params.animal)(req, res, next) 1020 | }, saw) 1021 | 1022 | series([ 1023 | function (cb) { 1024 | request(server) 1025 | .get('/') 1026 | .expect(404, cb) 1027 | }, 1028 | function (cb) { 1029 | request(server) 1030 | .get('/foo') 1031 | .expect(shouldHitHandle('foo')) 1032 | .expect(200, 'saw GET /', cb) 1033 | }, 1034 | function (cb) { 1035 | request(server) 1036 | .get('/zoo') 1037 | .expect(shouldHitHandle('zoo')) 1038 | .expect(200, 'saw GET /', cb) 1039 | }, 1040 | function (cb) { 1041 | request(server) 1042 | .get('/fooo') 1043 | .expect(404, cb) 1044 | }, 1045 | function (cb) { 1046 | request(server) 1047 | .get('/zoo/bear') 1048 | .expect(shouldHitHandle('zoobear')) 1049 | .expect(200, cb) 1050 | }, 1051 | function (cb) { 1052 | request(server) 1053 | .get('/get/zoo') 1054 | .expect(404, cb) 1055 | } 1056 | ], done) 1057 | }) 1058 | 1059 | it('should ensure regexp matches path prefix', function (done) { 1060 | const router = new Router() 1061 | const server = createServer(router) 1062 | 1063 | router.use(/\/api.*/, createHitHandle(1)) 1064 | router.use(/api/, createHitHandle(2)) 1065 | router.use(/\/test/, createHitHandle(3)) 1066 | router.use(helloWorld) 1067 | 1068 | request(server) 1069 | .get('/test/api/1234') 1070 | .expect(shouldNotHitHandle(1)) 1071 | .expect(shouldNotHitHandle(2)) 1072 | .expect(shouldHitHandle(3)) 1073 | .expect(200, done) 1074 | }) 1075 | 1076 | it('should support parameterized path', function (done) { 1077 | const router = new Router() 1078 | const server = createServer(router) 1079 | 1080 | router.use('/:thing', saw) 1081 | series([ 1082 | function (cb) { 1083 | request(server) 1084 | .get('/') 1085 | .expect(404, cb) 1086 | }, 1087 | function (cb) { 1088 | request(server) 1089 | .get('/foo') 1090 | .expect(200, 'saw GET /', cb) 1091 | }, 1092 | function (cb) { 1093 | request(server) 1094 | .get('/bar') 1095 | .expect(200, 'saw GET /', cb) 1096 | }, 1097 | function (cb) { 1098 | request(server) 1099 | .get('/foo/bar') 1100 | .expect(200, 'saw GET /bar', cb) 1101 | } 1102 | ], done) 1103 | }) 1104 | 1105 | it('should accept multiple arguments', function (done) { 1106 | const router = new Router() 1107 | const server = createServer(router) 1108 | 1109 | router.use('/foo', createHitHandle(1), createHitHandle(2), helloWorld) 1110 | 1111 | request(server) 1112 | .get('/foo') 1113 | .expect(shouldHitHandle(1)) 1114 | .expect(shouldHitHandle(2)) 1115 | .expect(200, 'hello, world', done) 1116 | }) 1117 | 1118 | describe('with "caseSensitive" option', function () { 1119 | it('should not match paths case-sensitively by default', function (done) { 1120 | const router = new Router() 1121 | const server = createServer(router) 1122 | 1123 | router.use('/foo', saw) 1124 | series([ 1125 | function (cb) { 1126 | request(server) 1127 | .get('/foo/bar') 1128 | .expect(200, 'saw GET /bar', cb) 1129 | }, 1130 | function (cb) { 1131 | request(server) 1132 | .get('/FOO/bar') 1133 | .expect(200, 'saw GET /bar', cb) 1134 | }, 1135 | function (cb) { 1136 | request(server) 1137 | .get('/FOO/BAR') 1138 | .expect(200, 'saw GET /BAR', cb) 1139 | } 1140 | ], done) 1141 | }) 1142 | 1143 | it('should not match paths case-sensitively when false', function (done) { 1144 | const router = new Router({ caseSensitive: false }) 1145 | const server = createServer(router) 1146 | 1147 | router.use('/foo', saw) 1148 | series([ 1149 | function (cb) { 1150 | request(server) 1151 | .get('/foo/bar') 1152 | .expect(200, 'saw GET /bar', cb) 1153 | }, 1154 | function (cb) { 1155 | request(server) 1156 | .get('/FOO/bar') 1157 | .expect(200, 'saw GET /bar', cb) 1158 | }, 1159 | function (cb) { 1160 | request(server) 1161 | .get('/FOO/BAR') 1162 | .expect(200, 'saw GET /BAR', cb) 1163 | } 1164 | ], done) 1165 | }) 1166 | 1167 | it('should match paths case-sensitively when true', function (done) { 1168 | const router = new Router({ caseSensitive: true }) 1169 | const server = createServer(router) 1170 | 1171 | router.use('/foo', saw) 1172 | series([ 1173 | function (cb) { 1174 | request(server) 1175 | .get('/foo/bar') 1176 | .expect(200, 'saw GET /bar', cb) 1177 | }, 1178 | function (cb) { 1179 | request(server) 1180 | .get('/FOO/bar') 1181 | .expect(404, cb) 1182 | }, 1183 | function (cb) { 1184 | request(server) 1185 | .get('/FOO/BAR') 1186 | .expect(404, cb) 1187 | } 1188 | ], done) 1189 | }) 1190 | }) 1191 | 1192 | describe('with "strict" option', function () { 1193 | it('should accept optional trailing slashes by default', function (done) { 1194 | const router = new Router() 1195 | const server = createServer(router) 1196 | 1197 | router.use('/foo', saw) 1198 | series([ 1199 | function (cb) { 1200 | request(server) 1201 | .get('/foo') 1202 | .expect(200, 'saw GET /', cb) 1203 | }, 1204 | function (cb) { 1205 | request(server) 1206 | .get('/foo/') 1207 | .expect(200, 'saw GET /', cb) 1208 | } 1209 | ], done) 1210 | }) 1211 | 1212 | it('should accept optional trailing slashes when false', function (done) { 1213 | const router = new Router({ strict: false }) 1214 | const server = createServer(router) 1215 | 1216 | router.use('/foo', saw) 1217 | series([ 1218 | function (cb) { 1219 | request(server) 1220 | .get('/foo') 1221 | .expect(200, 'saw GET /', cb) 1222 | }, 1223 | function (cb) { 1224 | request(server) 1225 | .get('/foo/') 1226 | .expect(200, 'saw GET /', cb) 1227 | } 1228 | ], done) 1229 | }) 1230 | 1231 | it('should accept optional trailing slashes when true', function (done) { 1232 | const router = new Router({ strict: true }) 1233 | const server = createServer(router) 1234 | 1235 | router.use('/foo', saw) 1236 | series([ 1237 | function (cb) { 1238 | request(server) 1239 | .get('/foo') 1240 | .expect(200, 'saw GET /', cb) 1241 | }, 1242 | function (cb) { 1243 | request(server) 1244 | .get('/foo/') 1245 | .expect(200, 'saw GET /', cb) 1246 | } 1247 | ], done) 1248 | }) 1249 | }) 1250 | 1251 | describe('next("route")', function () { 1252 | it('should invoke next handler', function (done) { 1253 | const router = new Router() 1254 | const server = createServer(router) 1255 | 1256 | router.use('/foo', function handle (req, res, next) { 1257 | res.setHeader('x-next', 'route') 1258 | next('route') 1259 | }) 1260 | 1261 | router.use('/foo', saw) 1262 | 1263 | request(server) 1264 | .get('/foo') 1265 | .expect('x-next', 'route') 1266 | .expect(200, 'saw GET /', done) 1267 | }) 1268 | 1269 | it('should invoke next function', function (done) { 1270 | const router = new Router() 1271 | const server = createServer(router) 1272 | 1273 | function goNext (req, res, next) { 1274 | res.setHeader('x-next', 'route') 1275 | next('route') 1276 | } 1277 | 1278 | router.use('/foo', createHitHandle(1), goNext, createHitHandle(2), saw) 1279 | 1280 | request(server) 1281 | .get('/foo') 1282 | .expect(shouldHitHandle(1)) 1283 | .expect('x-next', 'route') 1284 | .expect(shouldHitHandle(2)) 1285 | .expect(200, 'saw GET /', done) 1286 | }) 1287 | }) 1288 | 1289 | describe('req.baseUrl', function () { 1290 | it('should contain the stripped path', function (done) { 1291 | const router = new Router() 1292 | const server = createServer(router) 1293 | 1294 | router.use('/foo', sawBase) 1295 | 1296 | request(server) 1297 | .get('/foo/bar') 1298 | .expect(200, 'saw /foo', done) 1299 | }) 1300 | 1301 | it('should contain the stripped path for multiple levels', function (done) { 1302 | const router1 = new Router() 1303 | const router2 = new Router() 1304 | const server = createServer(router1) 1305 | 1306 | router1.use('/foo', router2) 1307 | router2.use('/bar', sawBase) 1308 | 1309 | request(server) 1310 | .get('/foo/bar/baz') 1311 | .expect(200, 'saw /foo/bar', done) 1312 | }) 1313 | 1314 | it('should contain the stripped path for multiple levels with regular expressions', function (done) { 1315 | const router1 = new Router() 1316 | const router2 = new Router() 1317 | const server = createServer(router1) 1318 | 1319 | router1.use(/^\/foo/, router2) 1320 | router2.use(/^\/bar/, sawBase) 1321 | 1322 | request(server) 1323 | .get('/foo/bar/baz') 1324 | .expect(200, 'saw /foo/bar', done) 1325 | }) 1326 | 1327 | it('should be altered correctly', function (done) { 1328 | const router = new Router() 1329 | const server = createServer(router) 1330 | const sub1 = new Router() 1331 | const sub2 = new Router() 1332 | const sub3 = new Router() 1333 | 1334 | sub3.get('/zed', setsawBase(1)) 1335 | 1336 | sub2.use('/baz', sub3) 1337 | 1338 | sub1.use('/', setsawBase(2)) 1339 | 1340 | sub1.use('/bar', sub2) 1341 | sub1.use('/bar', setsawBase(3)) 1342 | 1343 | router.use(setsawBase(4)) 1344 | router.use('/foo', sub1) 1345 | router.use(setsawBase(5)) 1346 | router.use(helloWorld) 1347 | 1348 | request(server) 1349 | .get('/foo/bar/baz/zed') 1350 | .expect('x-saw-base-1', '/foo/bar/baz') 1351 | .expect('x-saw-base-2', '/foo') 1352 | .expect('x-saw-base-3', '/foo/bar') 1353 | .expect('x-saw-base-4', '') 1354 | .expect('x-saw-base-5', '') 1355 | .expect(200, done) 1356 | }) 1357 | }) 1358 | 1359 | describe('req.url', function () { 1360 | it('should strip path from req.url', function (done) { 1361 | const router = new Router() 1362 | const server = createServer(router) 1363 | 1364 | router.use('/foo', saw) 1365 | 1366 | request(server) 1367 | .get('/foo/bar') 1368 | .expect(200, 'saw GET /bar', done) 1369 | }) 1370 | 1371 | it('should restore req.url after stripping', function (done) { 1372 | const router = new Router() 1373 | const server = createServer(router) 1374 | 1375 | router.use('/foo', setsaw(1)) 1376 | router.use(saw) 1377 | 1378 | request(server) 1379 | .get('/foo/bar') 1380 | .expect('x-saw-1', 'GET /bar') 1381 | .expect(200, 'saw GET /foo/bar', done) 1382 | }) 1383 | 1384 | it('should strip/restore with trailing stash', function (done) { 1385 | const router = new Router() 1386 | const server = createServer(router) 1387 | 1388 | router.use('/foo', setsaw(1)) 1389 | router.use(saw) 1390 | 1391 | request(server) 1392 | .get('/foo/') 1393 | .expect('x-saw-1', 'GET /') 1394 | .expect(200, 'saw GET /foo/', done) 1395 | }) 1396 | }) 1397 | }) 1398 | 1399 | describe('request rewriting', function () { 1400 | it('should support altering req.method', function (done) { 1401 | const router = new Router() 1402 | const server = createServer(router) 1403 | 1404 | router.put('/foo', createHitHandle(1)) 1405 | router.post('/foo', createHitHandle(2), function (req, res, next) { 1406 | req.method = 'PUT' 1407 | next() 1408 | }) 1409 | 1410 | router.post('/foo', createHitHandle(3)) 1411 | router.put('/foo', createHitHandle(4)) 1412 | router.use(saw) 1413 | 1414 | request(server) 1415 | .post('/foo') 1416 | .expect(shouldNotHitHandle(1)) 1417 | .expect(shouldHitHandle(2)) 1418 | .expect(shouldNotHitHandle(3)) 1419 | .expect(shouldHitHandle(4)) 1420 | .expect(200, 'saw PUT /foo', done) 1421 | }) 1422 | 1423 | it('should support altering req.url', function (done) { 1424 | const router = new Router() 1425 | const server = createServer(router) 1426 | 1427 | router.get('/bar', createHitHandle(1)) 1428 | router.get('/foo', createHitHandle(2), function (req, res, next) { 1429 | req.url = '/bar' 1430 | next() 1431 | }) 1432 | 1433 | router.get('/foo', createHitHandle(3)) 1434 | router.get('/bar', createHitHandle(4)) 1435 | router.use(saw) 1436 | 1437 | request(server) 1438 | .get('/foo') 1439 | .expect(shouldNotHitHandle(1)) 1440 | .expect(shouldHitHandle(2)) 1441 | .expect(shouldNotHitHandle(3)) 1442 | .expect(shouldHitHandle(4)) 1443 | .expect(200, 'saw GET /bar', done) 1444 | }) 1445 | }) 1446 | }) 1447 | 1448 | function helloWorld (req, res) { 1449 | res.statusCode = 200 1450 | res.setHeader('Content-Type', 'text/plain') 1451 | res.end('hello, world') 1452 | } 1453 | 1454 | function setsaw (num) { 1455 | const name = 'x-saw-' + String(num) 1456 | return function saw (req, res, next) { 1457 | res.setHeader(name, req.method + ' ' + req.url) 1458 | next() 1459 | } 1460 | } 1461 | 1462 | function setsawBase (num) { 1463 | const name = 'x-saw-base-' + String(num) 1464 | return function sawBase (req, res, next) { 1465 | res.setHeader(name, String(req.baseUrl)) 1466 | next() 1467 | } 1468 | } 1469 | 1470 | function saw (req, res) { 1471 | const msg = 'saw ' + req.method + ' ' + req.url 1472 | res.statusCode = 200 1473 | res.setHeader('Content-Type', 'text/plain') 1474 | res.end(msg) 1475 | } 1476 | 1477 | function sawError (err, req, res, next) { 1478 | const msg = 'saw ' + err.name + ': ' + err.message 1479 | res.statusCode = 200 1480 | res.setHeader('Content-Type', 'text/plain') 1481 | res.end(msg) 1482 | } 1483 | 1484 | function sawBase (req, res) { 1485 | const msg = 'saw ' + req.baseUrl 1486 | res.statusCode = 200 1487 | res.setHeader('Content-Type', 'text/plain') 1488 | res.end(msg) 1489 | } 1490 | -------------------------------------------------------------------------------- /test/support/utils.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const finalhandler = require('finalhandler') 3 | const http = require('http') 4 | const { METHODS } = require('node:http') 5 | const request = require('supertest') 6 | 7 | const methods = METHODS.map((method) => method.toLowerCase()) 8 | 9 | exports.assert = assert 10 | exports.createHitHandle = createHitHandle 11 | exports.createServer = createServer 12 | exports.rawrequest = rawrequest 13 | exports.request = request 14 | exports.shouldHaveBody = shouldHaveBody 15 | exports.shouldNotHaveBody = shouldNotHaveBody 16 | exports.shouldHitHandle = shouldHitHandle 17 | exports.shouldNotHitHandle = shouldNotHitHandle 18 | exports.methods = methods 19 | 20 | function createHitHandle (num) { 21 | const name = 'x-fn-' + String(num) 22 | return function hit (req, res, next) { 23 | res.setHeader(name, 'hit') 24 | next() 25 | } 26 | } 27 | 28 | function createServer (router) { 29 | return http.createServer(function onRequest (req, res) { 30 | router(req, res, finalhandler(req, res)) 31 | }) 32 | } 33 | 34 | function rawrequest (server) { 35 | const _headers = {} 36 | let _method 37 | let _path 38 | const _test = {} 39 | 40 | methods.forEach(function (method) { 41 | _test[method] = go.bind(null, method) 42 | }) 43 | 44 | function expect (status, body, callback) { 45 | if (arguments.length === 2) { 46 | _headers[status.toLowerCase()] = body 47 | return this 48 | } 49 | 50 | let _server 51 | 52 | if (!server.address()) { 53 | _server = server.listen(0, onListening) 54 | return 55 | } 56 | 57 | onListening.call(server) 58 | 59 | function onListening () { 60 | const addr = this.address() 61 | const port = addr.port 62 | 63 | const req = http.request({ 64 | host: '127.0.0.1', 65 | method: _method, 66 | path: _path, 67 | port 68 | }) 69 | req.on('response', function (res) { 70 | let buf = '' 71 | 72 | res.setEncoding('utf8') 73 | res.on('data', function (s) { buf += s }) 74 | res.on('end', function () { 75 | let err = null 76 | 77 | try { 78 | for (const key in _headers) { 79 | assert.equal(res.headers[key], _headers[key]) 80 | } 81 | 82 | assert.equal(res.statusCode, status) 83 | assert.equal(buf, body) 84 | } catch (e) { 85 | err = e 86 | } 87 | 88 | if (_server) { 89 | _server.close() 90 | } 91 | 92 | callback(err) 93 | }) 94 | }) 95 | req.end() 96 | } 97 | } 98 | 99 | function go (method, path) { 100 | _method = method 101 | _path = path 102 | 103 | return { 104 | expect 105 | } 106 | } 107 | 108 | return _test 109 | } 110 | 111 | function shouldHaveBody (buf) { 112 | return function (res) { 113 | const body = !Buffer.isBuffer(res.body) 114 | ? Buffer.from(res.text) 115 | : res.body 116 | assert.ok(body, 'response has body') 117 | assert.strictEqual(body.toString('hex'), buf.toString('hex')) 118 | } 119 | } 120 | 121 | function shouldHitHandle (num) { 122 | const header = 'x-fn-' + String(num) 123 | return function (res) { 124 | assert.equal(res.headers[header], 'hit', 'should hit handle ' + num) 125 | } 126 | } 127 | 128 | function shouldNotHaveBody () { 129 | return function (res) { 130 | assert.ok(res.text === '' || res.text === undefined) 131 | } 132 | } 133 | 134 | function shouldNotHitHandle (num) { 135 | return shouldNotHaveHeader('x-fn-' + String(num)) 136 | } 137 | 138 | function shouldNotHaveHeader (header) { 139 | return function (res) { 140 | assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header) 141 | } 142 | } 143 | --------------------------------------------------------------------------------