diff --git a/.editorconfig b/.editorconfig index cdb36c1b466..12cf1111232 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# http://editorconfig.org +# https://editorconfig.org root = true [*] diff --git a/.eslintrc.yml b/.eslintrc.yml index 587169c5330..70bc9a6e7e1 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,5 +1,7 @@ root: true - +env: + es2022: true + node: true rules: eol-last: error eqeqeq: [error, allow-null] diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..a6096a49b45 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + + - package-ecosystem: npm + directory: / + schedule: + interval: monthly + time: "23:00" + timezone: Europe/London + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01c82f81960..0421d562429 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,218 +1,114 @@ name: ci on: -- pull_request -- push + push: + branches: + - master + - develop + - '4.x' + - '5.x' + - '5.0' + paths-ignore: + - '*.md' + pull_request: + paths-ignore: + - '*.md' + +permissions: + contents: read + +# Cancel in progress workflows +# in the scenario where we already had a run going for that PR/branch/tag but then triggered a new run +concurrency: + group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true jobs: - test: + lint: + name: Lint runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup Node.js + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + with: + node-version: 'lts/*' + + - name: Install dependencies + run: npm install --ignore-scripts --include=dev + + - name: Run lint + run: npm run lint + + test: strategy: fail-fast: false matrix: - name: - - Node.js 0.10 - - Node.js 0.12 - - io.js 1.x - - io.js 2.x - - io.js 3.x - - Node.js 4.x - - Node.js 5.x - - Node.js 6.x - - Node.js 7.x - - Node.js 8.x - - Node.js 9.x - - Node.js 10.x - - Node.js 11.x - - Node.js 12.x - - Node.js 13.x - - Node.js 14.x - - Node.js 15.x - - Node.js 16.x - - Node.js 17.x - - Node.js 18.x - - Node.js 19.x - - Node.js 20.x - - Node.js 21.x - - include: - - name: Node.js 0.10 - node-version: "0.10" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: Node.js 0.12 - node-version: "0.12" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: io.js 1.x - node-version: "1.8" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: io.js 2.x - node-version: "2.5" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: io.js 3.x - node-version: "3.3" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: Node.js 4.x - node-version: "4.9" - npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 - - - name: Node.js 5.x - node-version: "5.12" - npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 - - - name: Node.js 6.x - node-version: "6.17" - npm-i: mocha@6.2.2 nyc@14.1.1 supertest@3.4.2 - - - name: Node.js 7.x - node-version: "7.10" - npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 - - - name: Node.js 8.x - node-version: "8.17" - npm-i: mocha@7.2.0 nyc@14.1.1 - - - name: Node.js 9.x - node-version: "9.11" - npm-i: mocha@7.2.0 nyc@14.1.1 - - - name: Node.js 10.x - node-version: "10.24" - npm-i: mocha@8.4.0 - - - name: Node.js 11.x - node-version: "11.15" - npm-i: mocha@8.4.0 - - - name: Node.js 12.x - node-version: "12.22" - npm-i: mocha@9.2.2 - - - name: Node.js 13.x - node-version: "13.14" - npm-i: mocha@9.2.2 - - - name: Node.js 14.x - node-version: "14.20" - - - name: Node.js 15.x - node-version: "15.14" - - - name: Node.js 16.x - node-version: "16.20" - - - name: Node.js 17.x - node-version: "17.9" - - - name: Node.js 18.x - node-version: "18.19" - - - name: Node.js 19.x - node-version: "19.9" - - - name: Node.js 20.x - node-version: "20.11" - - - name: Node.js 21.x - node-version: "21.6" + os: [ubuntu-latest, windows-latest] + node-version: [18, 19, 20, 21, 22, 23] + # Node.js release schedule: https://nodejs.org/en/about/releases/ + + name: Node.js ${{ matrix.node-version }} - ${{matrix.os}} + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - - name: Install Node.js ${{ matrix.node-version }} - shell: bash -eo pipefail -l {0} - run: | - nvm install --default ${{ matrix.node-version }} - dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" - - - name: Configure npm - run: | - npm config set loglevel error - if [[ "$(npm config get package-lock)" == "true" ]]; then - npm config set package-lock false - else - npm config set shrinkwrap false - fi - - - name: Install npm module(s) ${{ matrix.npm-i }} - run: npm install --save-dev ${{ matrix.npm-i }} - if: matrix.npm-i != '' - - - name: Remove non-test dependencies - run: npm rm --silent --save-dev connect-redis - - - name: Setup Node.js version-specific dependencies - shell: bash - run: | - # eslint for linting - # - remove on Node.js < 12 - if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then - node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ - grep -E '^eslint(-|$)' | \ - sort -r | \ - xargs -n1 npm rm --silent --save-dev - fi - - - name: Install Node.js dependencies - run: npm install - - - name: List environment - id: list_env - shell: bash - run: | - echo "node@$(node -v)" - echo "npm@$(npm -v)" - npm -s ls ||: - (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" - - - name: Run tests - shell: bash - run: | - npm run test-ci - cp coverage/lcov.info "coverage/${{ matrix.name }}.lcov" - - - name: Lint code - if: steps.list_env.outputs.eslint != '' - run: npm run lint - - - name: Collect code coverage - run: | - mv ./coverage "./${{ matrix.name }}" - mkdir ./coverage - mv "./${{ matrix.name }}" "./coverage/${{ matrix.name }}" - - - name: Upload code coverage - uses: actions/upload-artifact@v3 - with: - name: coverage - path: ./coverage - retention-days: 1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + with: + node-version: ${{ matrix.node-version }} + + - name: Configure npm loglevel + run: | + npm config set loglevel error + shell: bash + + - name: Install dependencies + run: npm install + + - name: Output Node and NPM versions + run: | + echo "Node.js version: $(node -v)" + echo "NPM version: $(npm -v)" + + - name: Run tests + shell: bash + run: npm run test-ci + + - name: Upload code coverage + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-node-${{ matrix.node-version }}-${{ matrix.os }} + path: ./coverage/lcov.info + retention-days: 1 coverage: needs: test runs-on: ubuntu-latest + permissions: + contents: read + checks: write steps: - - uses: actions/checkout@v4 - - - name: Install lcov - shell: bash - run: sudo apt-get -y install lcov - - - name: Collect coverage reports - uses: actions/download-artifact@v3 - with: - name: coverage - path: ./coverage - - - name: Merge coverage reports - shell: bash - run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./coverage/lcov.info - - - name: Upload coverage report - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install lcov + shell: bash + run: sudo apt-get -y install lcov + + - name: Collect coverage reports + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + with: + path: ./coverage + pattern: coverage-node-* + + - name: Merge coverage reports + shell: bash + run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info + + - name: Upload coverage report + uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 + with: + file: ./lcov.info diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..d3e37aa4d6a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,66 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master"] + schedule: + - cron: "0 0 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + with: + languages: javascript + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + with: + category: "/language:javascript" diff --git a/.github/workflows/legacy.yml b/.github/workflows/legacy.yml new file mode 100644 index 00000000000..81ffb94d748 --- /dev/null +++ b/.github/workflows/legacy.yml @@ -0,0 +1,98 @@ +name: legacy + +on: + push: + branches: + - master + - develop + - '4.x' + - '5.x' + - '5.0' + paths-ignore: + - '*.md' + pull_request: + paths-ignore: + - '*.md' + +permissions: + contents: read + +# Cancel in progress workflows +# in the scenario where we already had a run going for that PR/branch/tag but then triggered a new run +concurrency: + group: "${{ github.workflow }} ✨ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + node-version: [16, 17] + # Node.js release schedule: https://nodejs.org/en/about/releases/ + + name: Node.js ${{ matrix.node-version }} - ${{matrix.os}} + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + with: + node-version: ${{ matrix.node-version }} + + - name: Configure npm loglevel + run: | + npm config set loglevel error + shell: bash + + - name: Install dependencies + run: npm install + + - name: Output Node and NPM versions + run: | + echo "Node.js version: $(node -v)" + echo "NPM version: $(npm -v)" + + - name: Run tests + shell: bash + run: npm run test-ci + + - name: Upload code coverage + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-node-${{ matrix.node-version }}-${{ matrix.os }} + path: ./coverage/lcov.info + retention-days: 1 + + coverage: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + checks: write + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install lcov + shell: bash + run: sudo apt-get -y install lcov + + - name: Collect coverage reports + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + with: + path: ./coverage + pattern: coverage-node-* + + - name: Merge coverage reports + shell: bash + run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info + + - name: Upload coverage report + uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 + with: + file: ./lcov.info diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000000..20c76dae0db --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,72 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '16 21 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 3a673d9cc09..1bd5c02b28b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # npm node_modules package-lock.json +npm-shrinkwrap.json *.log *.gz diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..43c97e719a5 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/Charter.md b/Charter.md index a906e52909a..6227d6f6c85 100644 --- a/Charter.md +++ b/Charter.md @@ -20,11 +20,11 @@ alike. Express is made of many modules spread between three GitHub Orgs: -- [expressjs](http://github.com/expressjs/): Top level middleware and +- [expressjs](https://github.com/expressjs/): Top level middleware and libraries -- [pillarjs](http://github.com/pillarjs/): Components which make up +- [pillarjs](https://github.com/pillarjs/): Components which make up Express but can also be used for other web frameworks -- [jshttp](http://github.com/jshttp/): Low level HTTP libraries +- [jshttp](https://github.com/jshttp/): Low level HTTP libraries ### 1.2: Out-of-Scope diff --git a/Code-Of-Conduct.md b/Code-Of-Conduct.md index bbb8996a659..ca4c6b31468 100644 --- a/Code-Of-Conduct.md +++ b/Code-Of-Conduct.md @@ -127,7 +127,7 @@ project community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant, version 2.0](cc-20-doc). +This Code of Conduct is adapted from the [Contributor Covenant, version 2.0][cc-20-doc]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). diff --git a/Collaborator-Guide.md b/Collaborator-Guide.md index 3c73307d61b..ef8b6581448 100644 --- a/Collaborator-Guide.md +++ b/Collaborator-Guide.md @@ -7,7 +7,7 @@ Open issues for the expressjs.com website in https://github.com/expressjs/expres ## PRs and Code contributions * Tests must pass. -* Follow the [JavaScript Standard Style](http://standardjs.com/) and `npm run lint`. +* Follow the [JavaScript Standard Style](https://standardjs.com/) and `npm run lint`. * If you fix a bug, add a test. ## Branches diff --git a/Contributing.md b/Contributing.md index a9ba84690cb..a93da2349df 100644 --- a/Contributing.md +++ b/Contributing.md @@ -63,33 +63,19 @@ compromise among committers be the default resolution mechanism. Anyone can become a triager! Read more about the process of being a triager in [the triage process document](Triager-Guide.md). -[Open an issue in `expressjs/express` repo](https://github.com/expressjs/express/issues/new) -to request the triage role. State that you have read and agree to the -[Code of Conduct](Code-Of-Conduct.md) and details of the role. +Currently, any existing [organization member](https://github.com/orgs/expressjs/people) can nominate +a new triager. If you are interested in becoming a triager, our best advice is to actively participate +in the community by helping triaging issues and pull requests. As well we recommend +to engage in other community activities like attending the TC meetings, and participating in the Slack +discussions. If you feel ready and have been helping triage some issues, reach out to an active member of the organization to ask if they'd +be willing to support you. If they agree, they can create a pull request to formalize your nomination. In the case of an objection to the nomination, the triage team is responsible for working with the individuals involved and finding a resolution. -Here is an example issue content you can copy and paste: - -``` -Title: Request triager role for - -I have read and understood the project's Code of Conduct. -I also have read and understood the process and best practices around Express triaging. - -I request for a triager role for the following GitHub organizations: - -jshttp -pillarjs -express -``` - -Once you have opened your issue, a member of the TC will add you to the `triage` team in -the organizations requested. They will then close the issue. - -Happy triaging! +You can also reach out to any of the [organization members](https://github.com/orgs/expressjs/people) +if you have questions or need guidance. ## Becoming a Committer -All contributors who land a non-trivial contribution should be on-boarded in a timely manner, +All contributors who have landed significant and valuable contributions should be onboarded in a timely manner, and added as a committer, and be given write access to the repository. Committers are expected to follow this policy and continue to send pull requests, go through @@ -147,30 +133,113 @@ Like TC members, Repo captains are a subset of committers. To become a captain for a project the candidate is expected to participate in that project for at least 6 months as a committer prior to the request. They should have helped with code contributions as well as triaging issues. They are also required to -have 2FA enabled on both their GitHub and npm accounts. Any TC member or existing -captain on the repo can nominate another committer to the captain role, submit a PR to -this doc, under `Current Project Captains` section (maintaining the sort order) with -the project, their GitHub handle and npm username (if different). The PR will require -at least 2 approvals from TC members and 2 weeks hold time to allow for comment and/or -dissent. When the PR is merged, a TC member will add them to the proper GitHub/npm groups. - -### Current Project Captains - -- `expressjs/express`: @wesleytodd -- `expressjs/discussions`: @wesleytodd -- `expressjs/expressjs.com`: @crandmck -- `expressjs/body-parser`: @wesleytodd -- `expressjs/multer`: @LinusU -- `expressjs/cookie-parser`: @wesleytodd -- `expressjs/generator`: @wesleytodd -- `expressjs/statusboard`: @wesleytodd -- `pillarjs/path-to-regexp`: @blakeembrey -- `pillarjs/router`: @dougwilson, @wesleytodd -- `pillarjs/finalhandler`: @wesleytodd -- `pillarjs/request`: @wesleytodd -- `jshttp/http-errors`: @wesleytodd -- `jshttp/cookie`: @wesleytodd -- `jshttp/on-finished`: @wesleytodd -- `jshttp/forwarded`: @wesleytodd -- `jshttp/proxy-addr`: @wesleytodd - +have 2FA enabled on both their GitHub and npm accounts. + +Any TC member or an existing captain on the **same** repo can nominate another committer +to the captain role. To do so, they should submit a PR to this document, updating the +**Active Project Captains** section (while maintaining the sort order) with the project +name, the nominee's GitHub handle, and their npm username (if different). +- Repos can have as many captains as make sense for the scope of work. +- A TC member or an existing repo captain **on the same project** can nominate a new captain. + Repo captains from other projects should not nominate captains for a different project. + +The PR will require at least 2 approvals from TC members and 2 weeks hold time to allow +for comment and/or dissent. When the PR is merged, a TC member will add them to the +proper GitHub/npm groups. + +### Active Projects and Captains + +- [`expressjs/badgeboard`](https://github.com/expressjs/badgeboard): @wesleytodd +- [`expressjs/basic-auth-connect`](https://github.com/expressjs/basic-auth-connect): @ulisesGascon +- [`expressjs/body-parser`](https://github.com/expressjs/body-parser): @wesleytodd, @jonchurch, @ulisesGascon +- [`expressjs/compression`](https://github.com/expressjs/compression): @ulisesGascon +- [`expressjs/connect-multiparty`](https://github.com/expressjs/connect-multiparty): @ulisesGascon +- [`expressjs/cookie-parser`](https://github.com/expressjs/cookie-parser): @wesleytodd, @UlisesGascon +- [`expressjs/cookie-session`](https://github.com/expressjs/cookie-session): @ulisesGascon +- [`expressjs/cors`](https://github.com/expressjs/cors): @jonchurch, @ulisesGascon +- [`expressjs/discussions`](https://github.com/expressjs/discussions): @wesleytodd +- [`expressjs/errorhandler`](https://github.com/expressjs/errorhandler): @ulisesGascon +- [`expressjs/express-paginate`](https://github.com/expressjs/express-paginate): @ulisesGascon +- [`expressjs/express`](https://github.com/expressjs/express): @wesleytodd, @ulisesGascon +- [`expressjs/expressjs.com`](https://github.com/expressjs/expressjs.com): @crandmck, @jonchurch, @bjohansebas +- [`expressjs/flash`](https://github.com/expressjs/flash): @ulisesGascon +- [`expressjs/generator`](https://github.com/expressjs/generator): @wesleytodd +- [`expressjs/method-override`](https://github.com/expressjs/method-override): @ulisesGascon +- [`expressjs/morgan`](https://github.com/expressjs/morgan): @jonchurch, @ulisesGascon +- [`expressjs/multer`](https://github.com/expressjs/multer): @LinusU, @ulisesGascon +- [`expressjs/response-time`](https://github.com/expressjs/response-time): @UlisesGascon +- [`expressjs/serve-favicon`](https://github.com/expressjs/serve-favicon): @ulisesGascon +- [`expressjs/serve-index`](https://github.com/expressjs/serve-index): @ulisesGascon +- [`expressjs/serve-static`](https://github.com/expressjs/serve-static): @ulisesGascon +- [`expressjs/session`](https://github.com/expressjs/session): @ulisesGascon +- [`expressjs/statusboard`](https://github.com/expressjs/statusboard): @wesleytodd +- [`expressjs/timeout`](https://github.com/expressjs/timeout): @ulisesGascon +- [`expressjs/vhost`](https://github.com/expressjs/vhost): @ulisesGascon +- [`jshttp/accepts`](https://github.com/jshttp/accepts): @blakeembrey +- [`jshttp/basic-auth`](https://github.com/jshttp/basic-auth): @blakeembrey +- [`jshttp/compressible`](https://github.com/jshttp/compressible): @blakeembrey +- [`jshttp/content-disposition`](https://github.com/jshttp/content-disposition): @blakeembrey +- [`jshttp/content-type`](https://github.com/jshttp/content-type): @blakeembrey +- [`jshttp/cookie`](https://github.com/jshttp/cookie): @blakeembrey +- [`jshttp/etag`](https://github.com/jshttp/etag): @blakeembrey +- [`jshttp/forwarded`](https://github.com/jshttp/forwarded): @blakeembrey +- [`jshttp/fresh`](https://github.com/jshttp/fresh): @blakeembrey +- [`jshttp/http-assert`](https://github.com/jshttp/http-assert): @wesleytodd, @jonchurch, @ulisesGascon +- [`jshttp/http-errors`](https://github.com/jshttp/http-errors): @wesleytodd, @jonchurch, @ulisesGascon +- [`jshttp/media-typer`](https://github.com/jshttp/media-typer): @blakeembrey +- [`jshttp/methods`](https://github.com/jshttp/methods): @blakeembrey +- [`jshttp/mime-db`](https://github.com/jshttp/mime-db): @blakeembrey, @UlisesGascon +- [`jshttp/mime-types`](https://github.com/jshttp/mime-types): @blakeembrey, @UlisesGascon +- [`jshttp/negotiator`](https://github.com/jshttp/negotiator): @blakeembrey +- [`jshttp/on-finished`](https://github.com/jshttp/on-finished): @wesleytodd, @ulisesGascon +- [`jshttp/on-headers`](https://github.com/jshttp/on-headers): @blakeembrey +- [`jshttp/proxy-addr`](https://github.com/jshttp/proxy-addr): @wesleytodd, @ulisesGascon +- [`jshttp/range-parser`](https://github.com/jshttp/range-parser): @blakeembrey +- [`jshttp/statuses`](https://github.com/jshttp/statuses): @blakeembrey +- [`jshttp/type-is`](https://github.com/jshttp/type-is): @blakeembrey +- [`jshttp/vary`](https://github.com/jshttp/vary): @blakeembrey +- [`pillarjs/cookies`](https://github.com/pillarjs/cookies): @blakeembrey +- [`pillarjs/csrf`](https://github.com/pillarjs/csrf): @ulisesGascon +- [`pillarjs/encodeurl`](https://github.com/pillarjs/encodeurl): @blakeembrey +- [`pillarjs/finalhandler`](https://github.com/pillarjs/finalhandler): @wesleytodd, @ulisesGascon +- [`pillarjs/hbs`](https://github.com/pillarjs/hbs): @ulisesGascon +- [`pillarjs/multiparty`](https://github.com/pillarjs/multiparty): @blakeembrey +- [`pillarjs/parseurl`](https://github.com/pillarjs/parseurl): @blakeembrey +- [`pillarjs/path-to-regexp`](https://github.com/pillarjs/path-to-regexp): @blakeembrey +- [`pillarjs/request`](https://github.com/pillarjs/request): @wesleytodd +- [`pillarjs/resolve-path`](https://github.com/pillarjs/resolve-path): @blakeembrey +- [`pillarjs/router`](https://github.com/pillarjs/router): @wesleytodd, @ulisesGascon +- [`pillarjs/send`](https://github.com/pillarjs/send): @blakeembrey +- [`pillarjs/understanding-csrf`](https://github.com/pillarjs/understanding-csrf): @ulisesGascon + +### Current Initiative Captains + +- Triage team [ref](https://github.com/expressjs/discussions/issues/227): @UlisesGascon + +## Developer's Certificate of Origin 1.1 + +```text +By making a contribution to this project, I certify that: + + (a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + + (b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + + (c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + + (d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` diff --git a/History.md b/History.md index ac2e7cf719d..5b6cba51ed8 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,246 @@ +5.1.0 / 2025-03-31 +======================== + +* Add support for `Uint8Array` in `res.send()` +* Add support for ETag option in `res.sendFile()` +* Add support for multiple links with the same rel in `res.links()` +* Add funding field to package.json +* perf: use loop for acceptParams +* refactor: prefix built-in node module imports +* deps: remove `setprototypeof` +* deps: remove `safe-buffer` +* deps: remove `utils-merge` +* deps: remove `methods` +* deps: remove `depd` +* deps: `debug@^4.4.0` +* deps: `body-parser@^2.2.0` +* deps: `router@^2.2.0` +* deps: `content-type@^1.0.5` +* deps: `finalhandler@^2.1.0` +* deps: `qs@^6.14.0` +* deps: `server-static@2.2.0` +* deps: `type-is@2.0.1` + +5.0.1 / 2024-10-08 +========== + +* Update `cookie` semver lock to address [CVE-2024-47764](https://nvd.nist.gov/vuln/detail/CVE-2024-47764) + +5.0.0 / 2024-09-10 +========================= +* remove: + - `path-is-absolute` dependency - use `path.isAbsolute` instead +* breaking: + * `res.status()` accepts only integers, and input must be greater than 99 and less than 1000 + * will throw a `RangeError: Invalid status code: ${code}. Status code must be greater than 99 and less than 1000.` for inputs outside this range + * will throw a `TypeError: Invalid status code: ${code}. Status code must be an integer.` for non integer inputs + * deps: send@1.0.0 + * `res.redirect('back')` and `res.location('back')` is no longer a supported magic string, explicitly use `req.get('Referrer') || '/'`. +* change: + - `res.clearCookie` will ignore user provided `maxAge` and `expires` options +* deps: cookie-signature@^1.2.1 +* deps: debug@4.3.6 +* deps: merge-descriptors@^2.0.0 +* deps: serve-static@^2.1.0 +* deps: qs@6.13.0 +* deps: accepts@^2.0.0 +* deps: mime-types@^3.0.0 + - `application/javascript` => `text/javascript` +* deps: type-is@^2.0.0 +* deps: content-disposition@^1.0.0 +* deps: finalhandler@^2.0.0 +* deps: fresh@^2.0.0 +* deps: body-parser@^2.0.1 +* deps: send@^1.1.0 + +5.0.0-beta.3 / 2024-03-25 +========================= + +This incorporates all changes after 4.19.1 up to 4.19.2. + +5.0.0-beta.2 / 2024-03-20 +========================= + +This incorporates all changes after 4.17.2 up to 4.19.1. + +5.0.0-beta.1 / 2022-02-14 +========================= + +This is the first Express 5.0 beta release, based off 4.17.2 and includes +changes from 5.0.0-alpha.8. + + * change: + - Default "query parser" setting to `'simple'` + - Requires Node.js 4+ + - Use `mime-types` for file to content type mapping + * deps: array-flatten@3.0.0 + * deps: body-parser@2.0.0-beta.1 + - `req.body` is no longer always initialized to `{}` + - `urlencoded` parser now defaults `extended` to `false` + - Use `on-finished` to determine when body read + * deps: router@2.0.0-beta.1 + - Add new `?`, `*`, and `+` parameter modifiers + - Internalize private `router.process_params` method + - Matching group expressions are only RegExp syntax + - Named matching groups no longer available by position in `req.params` + - Regular expressions can only be used in a matching group + - Remove `debug` dependency + - Special `*` path segment behavior removed + - deps: array-flatten@3.0.0 + - deps: parseurl@~1.3.3 + - deps: path-to-regexp@3.2.0 + - deps: setprototypeof@1.2.0 + * deps: send@1.0.0-beta.1 + - Change `dotfiles` option default to `'ignore'` + - Remove `hidden` option; use `dotfiles` option instead + - Use `mime-types` for file to content type mapping + - deps: debug@3.1.0 + * deps: serve-static@2.0.0-beta.1 + - Change `dotfiles` option default to `'ignore'` + - Remove `hidden` option; use `dotfiles` option instead + - Use `mime-types` for file to content type mapping + - Remove `express.static.mime` export; use `mime-types` package instead + - deps: send@1.0.0-beta.1 + +5.0.0-alpha.8 / 2020-03-25 +========================== + +This is the eighth Express 5.0 alpha release, based off 4.17.1 and includes +changes from 5.0.0-alpha.7. + +5.0.0-alpha.7 / 2018-10-26 +========================== + +This is the seventh Express 5.0 alpha release, based off 4.16.4 and includes +changes from 5.0.0-alpha.6. + +The major change with this alpha is the basic support for returned, rejected +Promises in the router. + + * remove: + - `path-to-regexp` dependency + * deps: debug@3.1.0 + - Add `DEBUG_HIDE_DATE` environment variable + - Change timer to per-namespace instead of global + - Change non-TTY date format + - Remove `DEBUG_FD` environment variable support + - Support 256 namespace colors + * deps: router@2.0.0-alpha.1 + - Add basic support for returned, rejected Promises + - Fix JSDoc for `Router` constructor + - deps: debug@3.1.0 + - deps: parseurl@~1.3.2 + - deps: setprototypeof@1.1.0 + - deps: utils-merge@1.0.1 + +5.0.0-alpha.6 / 2017-09-24 +========================== + +This is the sixth Express 5.0 alpha release, based off 4.15.5 and includes +changes from 5.0.0-alpha.5. + + * remove: + - `res.redirect(url, status)` signature - use `res.redirect(status, url)` + - `res.send(status, body)` signature - use `res.status(status).send(body)` + * deps: router@~1.3.1 + - deps: debug@2.6.8 + +5.0.0-alpha.5 / 2017-03-06 +========================== + +This is the fifth Express 5.0 alpha release, based off 4.15.2 and includes +changes from 5.0.0-alpha.4. + +5.0.0-alpha.4 / 2017-03-01 +========================== + +This is the fourth Express 5.0 alpha release, based off 4.15.0 and includes +changes from 5.0.0-alpha.3. + + * remove: + - Remove Express 3.x middleware error stubs + * deps: router@~1.3.0 + - Add `next("router")` to exit from router + - Fix case where `router.use` skipped requests routes did not + - Skip routing when `req.url` is not set + - Use `%o` in path debug to tell types apart + - deps: debug@2.6.1 + - deps: setprototypeof@1.0.3 + - perf: add fast match path for `*` route + +5.0.0-alpha.3 / 2017-01-28 +========================== + +This is the third Express 5.0 alpha release, based off 4.14.1 and includes +changes from 5.0.0-alpha.2. + + * remove: + - `res.json(status, obj)` signature - use `res.status(status).json(obj)` + - `res.jsonp(status, obj)` signature - use `res.status(status).jsonp(obj)` + - `res.vary()` (no arguments) -- provide a field name as an argument + * deps: array-flatten@2.1.1 + * deps: path-is-absolute@1.0.1 + * deps: router@~1.1.5 + - deps: array-flatten@2.0.1 + - deps: methods@~1.1.2 + - deps: parseurl@~1.3.1 + - deps: setprototypeof@1.0.2 + +5.0.0-alpha.2 / 2015-07-06 +========================== + +This is the second Express 5.0 alpha release, based off 4.13.1 and includes +changes from 5.0.0-alpha.1. + + * remove: + - `app.param(fn)` + - `req.param()` -- use `req.params`, `req.body`, or `req.query` instead + * change: + - `res.render` callback is always async, even for sync view engines + - The leading `:` character in `name` for `app.param(name, fn)` is no longer removed + - Use `router` module for routing + - Use `path-is-absolute` module for absolute path detection + +5.0.0-alpha.1 / 2014-11-06 +========================== + +This is the first Express 5.0 alpha release, based off 4.10.1. + + * remove: + - `app.del` - use `app.delete` + - `req.acceptsCharset` - use `req.acceptsCharsets` + - `req.acceptsEncoding` - use `req.acceptsEncodings` + - `req.acceptsLanguage` - use `req.acceptsLanguages` + - `res.json(obj, status)` signature - use `res.json(status, obj)` + - `res.jsonp(obj, status)` signature - use `res.jsonp(status, obj)` + - `res.send(body, status)` signature - use `res.send(status, body)` + - `res.send(status)` signature - use `res.sendStatus(status)` + - `res.sendfile` - use `res.sendFile` instead + - `express.query` middleware + * change: + - `req.host` now returns host (`hostname:port`) - use `req.hostname` for only hostname + - `req.query` is now a getter instead of a plain property + * add: + - `app.router` is a reference to the base router + +4.20.0 / 2024-09-10 +========== + * deps: serve-static@0.16.0 + * Remove link renderization in html while redirecting + * deps: send@0.19.0 + * Remove link renderization in html while redirecting + * deps: body-parser@0.6.0 + * add `depth` option to customize the depth level in the parser + * IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`) + * Remove link renderization in html while using `res.redirect` + * deps: path-to-regexp@0.1.10 + - Adds support for named matching groups in the routes using a regex + - Adds backtracking protection to parameters without regexes defined + * deps: encodeurl@~2.0.0 + - Removes encoding of `\`, `|`, and `^` to align better with URL spec + * Deprecate passing `options.maxAge` and `options.expires` to `res.clearCookie` + - Will be ignored in v5, clearCookie will set a cookie with an expires in the past to instruct clients to delete the cookie + 4.19.2 / 2024-03-25 ========== diff --git a/Readme.md b/Readme.md index d0f3cf56e6d..7443b818591 100644 --- a/Readme.md +++ b/Readme.md @@ -1,16 +1,35 @@ -[![Express Logo](https://i.cloudup.com/zfY6lL7eFa-3000x3000.png)](http://expressjs.com/) +[![Express Logo](https://i.cloudup.com/zfY6lL7eFa-3000x3000.png)](https://expressjs.com/) - Fast, unopinionated, minimalist web framework for [Node.js](http://nodejs.org). +**Fast, unopinionated, minimalist web framework for [Node.js](https://nodejs.org).** + +**This project has a [Code of Conduct][].** + +## Table of contents + +* [Installation](#Installation) +* [Features](#Features) +* [Docs & Community](#docs--community) +* [Quick Start](#Quick-Start) +* [Running Tests](#Running-Tests) +* [Philosophy](#Philosophy) +* [Examples](#Examples) +* [Contributing to Express](#Contributing) +* [TC (Technical Committee)](#tc-technical-committee) +* [Triagers](#triagers) +* [License](#license) + + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-downloads-url] +[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] - [![NPM Version][npm-version-image]][npm-url] - [![NPM Install Size][npm-install-size-image]][npm-install-size-url] - [![NPM Downloads][npm-downloads-image]][npm-downloads-url] ```js -const express = require('express') +import express from 'express' + const app = express() -app.get('/', function (req, res) { +app.get('/', (req, res) => { res.send('Hello World') }) @@ -23,7 +42,7 @@ This is a [Node.js](https://nodejs.org/en/) module available through the [npm registry](https://www.npmjs.com/). Before installing, [download and install Node.js](https://nodejs.org/en/download/). -Node.js 0.10 or higher is required. +Node.js 18 or higher is required. If this is a brand new project, make sure to create a `package.json` first with the [`npm init` command](https://docs.npmjs.com/creating-a-package-json-file). @@ -31,11 +50,11 @@ the [`npm init` command](https://docs.npmjs.com/creating-a-package-json-file). Installation is done using the [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): -```console -$ npm install express +```bash +npm install express ``` -Follow [our installing guide](http://expressjs.com/en/starter/installing.html) +Follow [our installing guide](https://expressjs.com/en/starter/installing.html) for more information. ## Features @@ -50,14 +69,11 @@ for more information. ## Docs & Community - * [Website and Documentation](http://expressjs.com/) - [[website repo](https://github.com/expressjs/expressjs.com)] - * [#express](https://web.libera.chat/#express) on [Libera Chat](https://libera.chat) IRC + * [Website and Documentation](https://expressjs.com/) - [[website repo](https://github.com/expressjs/expressjs.com)] * [GitHub Organization](https://github.com/expressjs) for Official Middleware & Modules - * Visit the [Wiki](https://github.com/expressjs/express/wiki) - * [Google Group](https://groups.google.com/group/express-js) for discussion - * [Gitter](https://gitter.im/expressjs/express) for support and discussion + * [Github Discussions](https://github.com/expressjs/discussions) for discussion on the development and usage of Express -**PROTIP** Be sure to read [Migrating from 3.x to 4.x](https://github.com/expressjs/express/wiki/Migrating-from-3.x-to-4.x) as well as [New features in 4.x](https://github.com/expressjs/express/wiki/New-features-in-4.x). +**PROTIP** Be sure to read the [migration guide to v5](https://expressjs.com/en/guide/migrating-5) ## Quick Start @@ -65,26 +81,26 @@ for more information. Install the executable. The executable's major version will match Express's: -```console -$ npm install -g express-generator@4 +```bash +npm install -g express-generator@4 ``` Create the app: -```console -$ express /tmp/foo && cd /tmp/foo +```bash +express /tmp/foo && cd /tmp/foo ``` Install dependencies: -```console -$ npm install +```bash +npm install ``` Start the server: -```console -$ npm start +```bash +npm start ``` View the website at: http://localhost:3000 @@ -96,29 +112,32 @@ $ npm start HTTP APIs. Express does not force you to use any specific ORM or template engine. With support for over - 14 template engines via [Consolidate.js](https://github.com/tj/consolidate.js), + 14 template engines via [@ladjs/consolidate](https://github.com/ladjs/consolidate), you can quickly craft your perfect framework. ## Examples - To view the examples, clone the Express repo and install the dependencies: + To view the examples, clone the Express repository: -```console -$ git clone https://github.com/expressjs/express.git --depth 1 -$ cd express -$ npm install +```bash +git clone https://github.com/expressjs/express.git --depth 1 && cd express +``` + + Then install the dependencies: + +```bash +npm install ``` Then run whichever example you want: -```console -$ node examples/content-negotiation +```bash +node examples/content-negotiation ``` ## Contributing [![Linux Build][github-actions-ci-image]][github-actions-ci-url] - [![Windows Build][appveyor-image]][appveyor-url] [![Test Coverage][coveralls-image]][coveralls-url] The Express.js project welcomes all constructive contributions. Contributions take many forms, @@ -133,34 +152,115 @@ If you discover a security vulnerability in Express, please see [Security Polici ### Running Tests -To run the test suite, first install the dependencies, then run `npm test`: +To run the test suite, first install the dependencies: + +```bash +npm install +``` + +Then run `npm test`: -```console -$ npm install -$ npm test +```bash +npm test ``` ## People The original author of Express is [TJ Holowaychuk](https://github.com/tj) -The current lead maintainer is [Douglas Christopher Wilson](https://github.com/dougwilson) - [List of all contributors](https://github.com/expressjs/express/graphs/contributors) +### TC (Technical Committee) + +* [UlisesGascon](https://github.com/UlisesGascon) - **Ulises Gascón** (he/him) +* [jonchurch](https://github.com/jonchurch) - **Jon Church** +* [wesleytodd](https://github.com/wesleytodd) - **Wes Todd** +* [LinusU](https://github.com/LinusU) - **Linus Unnebäck** +* [blakeembrey](https://github.com/blakeembrey) - **Blake Embrey** +* [sheplu](https://github.com/sheplu) - **Jean Burellier** +* [crandmck](https://github.com/crandmck) - **Rand McKinney** +* [ctcpip](https://github.com/ctcpip) - **Chris de Almeida** + +
+TC emeriti members + +#### TC emeriti members + + * [dougwilson](https://github.com/dougwilson) - **Douglas Wilson** + * [hacksparrow](https://github.com/hacksparrow) - **Hage Yaapa** + * [jonathanong](https://github.com/jonathanong) - **jongleberry** + * [niftylettuce](https://github.com/niftylettuce) - **niftylettuce** + * [troygoode](https://github.com/troygoode) - **Troy Goode** +
+ + +### Triagers + +* [aravindvnair99](https://github.com/aravindvnair99) - **Aravind Nair** +* [bjohansebas](https://github.com/bjohansebas) - **Sebastian Beltran** +* [carpasse](https://github.com/carpasse) - **Carlos Serrano** +* [CBID2](https://github.com/CBID2) - **Christine Belzie** +* [dpopp07](https://github.com/dpopp07) - **Dustin Popp** +* [UlisesGascon](https://github.com/UlisesGascon) - **Ulises Gascón** (he/him) +* [3imed-jaberi](https://github.com/3imed-jaberi) - **Imed Jaberi** +* [IamLizu](https://github.com/IamLizu) - **S M Mahmudul Hasan** (he/him) +* [Phillip9587](https://github.com/Phillip9587) - **Phillip Barta** +* [Sushmeet](https://github.com/Sushmeet) - **Sushmeet Sunger** +* [rxmarbles](https://github.com/rxmarbles) **Rick Markins** (He/him) + +
+Triagers emeriti members + +#### Emeritus Triagers + + * [AuggieH](https://github.com/AuggieH) - **Auggie Hudak** + * [G-Rath](https://github.com/G-Rath) - **Gareth Jones** + * [MohammadXroid](https://github.com/MohammadXroid) - **Mohammad Ayashi** + * [NawafSwe](https://github.com/NawafSwe) - **Nawaf Alsharqi** + * [NotMoni](https://github.com/NotMoni) - **Moni** + * [VigneshMurugan](https://github.com/VigneshMurugan) - **Vignesh Murugan** + * [davidmashe](https://github.com/davidmashe) - **David Ashe** + * [digitaIfabric](https://github.com/digitaIfabric) - **David** + * [e-l-i-s-e](https://github.com/e-l-i-s-e) - **Elise Bonner** + * [fed135](https://github.com/fed135) - **Frederic Charette** + * [firmanJS](https://github.com/firmanJS) - **Firman Abdul Hakim** + * [getspooky](https://github.com/getspooky) - **Yasser Ameur** + * [ghinks](https://github.com/ghinks) - **Glenn** + * [ghousemohamed](https://github.com/ghousemohamed) - **Ghouse Mohamed** + * [gireeshpunathil](https://github.com/gireeshpunathil) - **Gireesh Punathil** + * [jake32321](https://github.com/jake32321) - **Jake Reed** + * [jonchurch](https://github.com/jonchurch) - **Jon Church** + * [lekanikotun](https://github.com/lekanikotun) - **Troy Goode** + * [marsonya](https://github.com/marsonya) - **Lekan Ikotun** + * [mastermatt](https://github.com/mastermatt) - **Matt R. Wilson** + * [maxakuru](https://github.com/maxakuru) - **Max Edell** + * [mlrawlings](https://github.com/mlrawlings) - **Michael Rawlings** + * [rodion-arr](https://github.com/rodion-arr) - **Rodion Abdurakhimov** + * [sheplu](https://github.com/sheplu) - **Jean Burellier** + * [tarunyadav1](https://github.com/tarunyadav1) - **Tarun yadav** + * [tunniclm](https://github.com/tunniclm) - **Mike Tunnicliffe** + * [enyoghasim](https://github.com/enyoghasim) - **David Enyoghasim** + * [0ss](https://github.com/0ss) - **Salah** + * [import-brain](https://github.com/import-brain) - **Eric Cheng** (he/him) + * [dakshkhetan](https://github.com/dakshkhetan) - **Daksh Khetan** (he/him) + * [lucasraziel](https://github.com/lucasraziel) - **Lucas Soares Do Rego** + * [mertcanaltin](https://github.com/mertcanaltin) - **Mert Can Altin** + +
+ + ## License [MIT](LICENSE) -[appveyor-image]: https://badgen.net/appveyor/ci/dougwilson/express/master?label=windows -[appveyor-url]: https://ci.appveyor.com/project/dougwilson/express [coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/express/master [coveralls-url]: https://coveralls.io/r/expressjs/express?branch=master -[github-actions-ci-image]: https://badgen.net/github/checks/expressjs/express/master?label=linux +[github-actions-ci-image]: https://badgen.net/github/checks/expressjs/express/master?label=CI [github-actions-ci-url]: https://github.com/expressjs/express/actions/workflows/ci.yml [npm-downloads-image]: https://badgen.net/npm/dm/express [npm-downloads-url]: https://npmcharts.com/compare/express?minimal=true -[npm-install-size-image]: https://badgen.net/packagephobia/install/express -[npm-install-size-url]: https://packagephobia.com/result?p=express [npm-url]: https://npmjs.org/package/express [npm-version-image]: https://badgen.net/npm/v/express +[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/express/badge +[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/express +[Code of Conduct]: https://github.com/expressjs/express/blob/master/Code-Of-Conduct.md diff --git a/Release-Process.md b/Release-Process.md index 55e62189250..e332820f052 100644 --- a/Release-Process.md +++ b/Release-Process.md @@ -31,7 +31,7 @@ Before publishing, the following preconditions should be met: below) will exist documenting: - the proposed changes - the type of release: patch, minor or major - - the version number (according to semantic versioning - http://semver.org) + - the version number (according to semantic versioning - https://semver.org) - The proposed changes should be complete. There are two main release flows: patch and non-patch. @@ -77,6 +77,13 @@ non-patch flow. - This branch contains the commits accepted so far that implement the proposal in the tracking pull request. +### Pre-release Versions + +Alpha and Beta releases are made from a proposal branch. The version number should be +incremented to the next minor version with a `-beta` or `-alpha` suffix. +For example, if the next beta release is `5.0.1`, the beta release would be `5.0.1-beta.0`. +The pre-releases are unstable and not suitable for production use. + ### Patch flow In the patch flow, simple changes are committed to the release branch which @@ -122,9 +129,10 @@ $ git merge --ff-only - see "Release branch" of "Branches" above. - see "Proposal branch" of "Non-patch flow" above. -**NOTE:** You may need to rebase the proposal branch to allow a fast-forward - merge. Using a fast-forward merge keeps the history clean as it does - not introduce merge commits. +> [!NOTE] +> You may need to rebase the proposal branch to allow a fast-forward +> merge. Using a fast-forward merge keeps the history clean as it does +> not introduce merge commits. ### Step 3. Update the History.md and package.json to the new version number @@ -182,11 +190,13 @@ $ npm login $ npm publish ``` -**NOTE:** The version number to publish will be picked up automatically from - package.json. +> [!NOTE] +> The version number to publish will be picked up automatically from +> package.json. ### Step 7. Update documentation website -The documentation website https://expressjs.com/ documents the current release version in various places. For a new release: -1. Change the value of `current_version` in https://github.com/expressjs/expressjs.com/blob/gh-pages/_data/express.yml to match the latest version number. -2. Add a new section to the change log. For example, for a 4.x release, https://github.com/expressjs/expressjs.com/blob/gh-pages/en/changelog/4x.md, +The documentation website https://expressjs.com/ documents the current release version in various places. To update these, follow these steps: + +1. Manually run the [`Update External Docs` workflow](https://github.com/expressjs/expressjs.com/actions/workflows/update-external-docs.yml) in expressjs.com repository. +2. Add a new section to the [changelog](https://github.com/expressjs/expressjs.com/blob/gh-pages/en/changelog/index.md) in the expressjs.com website. diff --git a/Security.md b/Security.md index cdcd7a6e0aa..ff106d62104 100644 --- a/Security.md +++ b/Security.md @@ -14,7 +14,7 @@ Thank you for improving the security of Express. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. -Report security bugs by emailing the lead maintainer in the Readme.md file. +Report security bugs by emailing `express-security@lists.openjsf.org`. To ensure the timely response to your report, please ensure that the entirety of the report is contained within the email body and not solely behind a web @@ -29,6 +29,12 @@ announcement, and may ask for additional information or guidance. Report security bugs in third-party modules to the person or team maintaining the module. +## Pre-release Versions + +Alpha and Beta releases are unstable and **not suitable for production use**. +Vulnerabilities found in pre-releases should be reported according to the [Reporting a Bug](#reporting-a-bug) section. +Due to the unstable nature of the branch it is not guaranteed that any fixes will be released in the next pre-release. + ## Disclosure Policy When the security team receives a security bug report, they will assign it to a @@ -40,6 +46,10 @@ involving the following steps: * Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible to npm. +## The Express Threat Model + +We are currently working on a new version of the security model, the most updated version can be found [here](https://github.com/expressjs/security-wg/blob/main/docs/ThreatModel.md) + ## Comments on this Policy If you have suggestions on how this process could be improved please submit a diff --git a/Triager-Guide.md b/Triager-Guide.md index a2909ef30db..65aff265f01 100644 --- a/Triager-Guide.md +++ b/Triager-Guide.md @@ -9,11 +9,18 @@ classification: * `needs triage`: This can be kept if the triager is unsure which next steps to take * `awaiting more info`: If more info has been requested from the author, apply this label. -* `question`: User questions that do not appear to be bugs or enhancements. -* `discuss`: Topics for discussion. Might end in an `enhancement` or `question` label. * `bug`: Issues that present a reasonable conviction there is a reproducible bug. * `enhancement`: Issues that are found to be a reasonable candidate feature additions. +If the issue is a question or discussion, it should be moved to GitHub Discussions. + +### Moving Discussions and Questions to GitHub Discussions + +For issues labeled with `question` or `discuss`, it is recommended to move them to GitHub Discussions instead: + +* **Questions**: User questions that do not appear to be bugs or enhancements should be moved to GitHub Discussions. +* **Discussions**: Topics for discussion should be moved to GitHub Discussions. If the discussion leads to a new feature or bug identification, it can be moved back to Issues. + In all cases, issues may be closed by maintainers if they don't receive a timely response when further information is sought, or when additional questions are asked. @@ -61,3 +68,5 @@ If you have questions feel free to reach out to any of the TC members. - For recurring issues, it is helpful to create functional examples to demonstrate (publish as gists or a repo) - Review and identify the maintainers. If necessary, at-mention one or more of them if you are unsure what to do - Make sure all your interactions are professional, welcoming, and respectful to the parties involved. +- When an issue refers to security concerns, responsibility is delegated to the repository captain or the security group in any public communication. + - If an issue has been open for a long time, the person in charge should be contacted internally through the private Slack chat. \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index ce26523b3aa..00000000000 --- a/appveyor.yml +++ /dev/null @@ -1,112 +0,0 @@ -environment: - matrix: - - nodejs_version: "0.10" - - nodejs_version: "0.12" - - nodejs_version: "1.8" - - nodejs_version: "2.5" - - nodejs_version: "3.3" - - nodejs_version: "4.9" - - nodejs_version: "5.12" - - nodejs_version: "6.17" - - nodejs_version: "7.10" - - nodejs_version: "8.17" - - nodejs_version: "9.11" - - nodejs_version: "10.24" - - nodejs_version: "11.15" - - nodejs_version: "12.22" - - nodejs_version: "13.14" - - nodejs_version: "14.20" - - nodejs_version: "15.14" - - nodejs_version: "16.20" - - nodejs_version: "17.9" - - nodejs_version: "18.19" - - nodejs_version: "19.9" - - nodejs_version: "20.11" - - nodejs_version: "21.6" -cache: - - node_modules -install: - # Install Node.js - - ps: >- - try { Install-Product node $env:nodejs_version -ErrorAction Stop } - catch { Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) x64 } - # Configure npm - - ps: | - npm config set loglevel error - if ((npm config get package-lock) -eq "true") { - npm config set package-lock false - } else { - npm config set shrinkwrap false - } - # Remove all non-test dependencies - - ps: | - # Remove example dependencies - npm rm --silent --save-dev connect-redis - # Remove lint dependencies - cmd.exe /c "node -pe `"Object.keys(require('./package').devDependencies).join('\n')`"" | ` - sls "^eslint(-|$)" | ` - %{ npm rm --silent --save-dev $_ } - # Setup Node.js version-specific dependencies - - ps: | - # mocha for testing - # - use 3.x for Node.js < 4 - # - use 5.x for Node.js < 6 - # - use 6.x for Node.js < 8 - # - use 7.x for Node.js < 10 - # - use 8.x for Node.js < 12 - # - use 9.x for Node.js < 14 - if ([int]$env:nodejs_version.split(".")[0] -lt 4) { - npm install --silent --save-dev mocha@3.5.3 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) { - npm install --silent --save-dev mocha@5.2.0 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { - npm install --silent --save-dev mocha@6.2.2 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 10) { - npm install --silent --save-dev mocha@7.2.0 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 12) { - npm install --silent --save-dev mocha@8.4.0 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 14) { - npm install --silent --save-dev mocha@9.2.2 - } - - ps: | - # nyc for test coverage - # - use 10.3.2 for Node.js < 4 - # - use 11.9.0 for Node.js < 6 - # - use 14.1.1 for Node.js < 10 - if ([int]$env:nodejs_version.split(".")[0] -lt 4) { - npm install --silent --save-dev nyc@10.3.2 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) { - npm install --silent --save-dev nyc@11.9.0 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 10) { - npm install --silent --save-dev nyc@14.1.1 - } - - ps: | - # supertest for http calls - # - use 2.0.0 for Node.js < 4 - # - use 3.4.2 for Node.js < 7 - # - use 6.1.6 for Node.js < 8 - if ([int]$env:nodejs_version.split(".")[0] -lt 4) { - npm install --silent --save-dev supertest@2.0.0 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 7) { - npm install --silent --save-dev supertest@3.4.2 - } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { - npm install --silent --save-dev supertest@6.1.6 - } - # Update Node.js modules - - ps: | - # Prune & rebuild node_modules - if (Test-Path -Path node_modules) { - npm prune - npm rebuild - } - # Install Node.js modules - - npm install -build: off -test_script: - # Output version data - - ps: | - node --version - npm --version - # Run test script - - npm run test-ci -version: "{build}" diff --git a/examples/auth/index.js b/examples/auth/index.js index 36205d0f994..40b73e6de16 100644 --- a/examples/auth/index.js +++ b/examples/auth/index.js @@ -6,7 +6,7 @@ var express = require('../..'); var hash = require('pbkdf2-password')() -var path = require('path'); +var path = require('node:path'); var session = require('express-session'); var app = module.exports = express(); @@ -18,7 +18,7 @@ app.set('views', path.join(__dirname, 'views')); // middleware -app.use(express.urlencoded({ extended: false })) +app.use(express.urlencoded()) app.use(session({ resave: false, // don't save session if unmodified saveUninitialized: false, // don't create session until something stored @@ -102,6 +102,7 @@ app.get('/login', function(req, res){ }); app.post('/login', function (req, res, next) { + if (!req.body) return res.sendStatus(400) authenticate(req.body.username, req.body.password, function(err, user){ if (err) return next(err) if (user) { @@ -115,7 +116,7 @@ app.post('/login', function (req, res, next) { req.session.success = 'Authenticated as ' + user.name + ' click to logout. ' + ' You may now access /restricted.'; - res.redirect('back'); + res.redirect(req.get('Referrer') || '/'); }); } else { req.session.error = 'Authentication failed, please check your ' diff --git a/examples/auth/views/head.ejs b/examples/auth/views/head.ejs index 65386267d0d..c623b5cc8d1 100644 --- a/examples/auth/views/head.ejs +++ b/examples/auth/views/head.ejs @@ -10,7 +10,7 @@ font: 13px Helvetica, Arial, sans-serif; } .error { - color: red + color: red; } .success { color: green; diff --git a/examples/cookies/index.js b/examples/cookies/index.js index 04093591f79..0620cb40e45 100644 --- a/examples/cookies/index.js +++ b/examples/cookies/index.js @@ -19,7 +19,7 @@ if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url')) app.use(cookieParser('my secret here')); // parses x-www-form-urlencoded -app.use(express.urlencoded({ extended: false })) +app.use(express.urlencoded()) app.get('/', function(req, res){ if (req.cookies.remember) { @@ -33,13 +33,17 @@ app.get('/', function(req, res){ app.get('/forget', function(req, res){ res.clearCookie('remember'); - res.redirect('back'); + res.redirect(req.get('Referrer') || '/'); }); app.post('/', function(req, res){ var minute = 60000; - if (req.body.remember) res.cookie('remember', 1, { maxAge: minute }); - res.redirect('back'); + + if (req.body && req.body.remember) { + res.cookie('remember', 1, { maxAge: minute }) + } + + res.redirect(req.get('Referrer') || '/'); }); /* istanbul ignore next */ diff --git a/examples/downloads/index.js b/examples/downloads/index.js index 6b67e0c8862..ddc549ffec7 100644 --- a/examples/downloads/index.js +++ b/examples/downloads/index.js @@ -5,7 +5,7 @@ */ var express = require('../../'); -var path = require('path'); +var path = require('node:path'); var app = module.exports = express(); @@ -23,8 +23,8 @@ app.get('/', function(req, res){ // /files/* is accessed via req.params[0] // but here we name it :file -app.get('/files/:file(*)', function(req, res, next){ - res.download(req.params.file, { root: FILES_DIR }, function (err) { +app.get('/files/*file', function (req, res, next) { + res.download(req.params.file.join('/'), { root: FILES_DIR }, function (err) { if (!err) return; // file sent if (err.status !== 404) return next(err); // non-404 error // file for download not found diff --git a/examples/ejs/index.js b/examples/ejs/index.js index a39d805a160..0940d0624fb 100644 --- a/examples/ejs/index.js +++ b/examples/ejs/index.js @@ -5,7 +5,7 @@ */ var express = require('../../'); -var path = require('path'); +var path = require('node:path'); var app = module.exports = express(); diff --git a/examples/error-pages/index.js b/examples/error-pages/index.js index efa815c4740..0863120bc8f 100644 --- a/examples/error-pages/index.js +++ b/examples/error-pages/index.js @@ -5,7 +5,7 @@ */ var express = require('../../'); -var path = require('path'); +var path = require('node:path'); var app = module.exports = express(); var logger = require('morgan'); var silent = process.env.NODE_ENV === 'test' diff --git a/examples/markdown/index.js b/examples/markdown/index.js index 23d645e66b2..53e40ac38e4 100644 --- a/examples/markdown/index.js +++ b/examples/markdown/index.js @@ -6,9 +6,9 @@ var escapeHtml = require('escape-html'); var express = require('../..'); -var fs = require('fs'); +var fs = require('node:fs'); var marked = require('marked'); -var path = require('path'); +var path = require('node:path'); var app = module.exports = express(); diff --git a/examples/mvc/index.js b/examples/mvc/index.js index da4727b282d..1d8aa0e3c31 100644 --- a/examples/mvc/index.js +++ b/examples/mvc/index.js @@ -6,7 +6,7 @@ var express = require('../..'); var logger = require('morgan'); -var path = require('path'); +var path = require('node:path'); var session = require('express-session'); var methodOverride = require('method-override'); diff --git a/examples/mvc/lib/boot.js b/examples/mvc/lib/boot.js index 0216e5d76d6..fc2ab0fad99 100644 --- a/examples/mvc/lib/boot.js +++ b/examples/mvc/lib/boot.js @@ -5,8 +5,8 @@ */ var express = require('../../..'); -var fs = require('fs'); -var path = require('path'); +var fs = require('node:fs'); +var path = require('node:path'); module.exports = function(parent, options){ var dir = path.join(__dirname, '..', 'controllers'); diff --git a/examples/params/index.js b/examples/params/index.js index f3cd8457eb5..11eef51a592 100644 --- a/examples/params/index.js +++ b/examples/params/index.js @@ -32,7 +32,8 @@ app.param(['to', 'from'], function(req, res, next, num, name){ // Load user by id app.param('user', function(req, res, next, id){ - if (req.user = users[id]) { + req.user = users[id] + if (req.user) { next(); } else { next(createError(404, 'failed to find user')); diff --git a/examples/resource/index.js b/examples/resource/index.js index ff1f6fe11f4..627ab24c5a2 100644 --- a/examples/resource/index.js +++ b/examples/resource/index.js @@ -12,7 +12,7 @@ var app = module.exports = express(); app.resource = function(path, obj) { this.get(path, obj.index); - this.get(path + '/:a..:b.:format?', function(req, res){ + this.get(path + '/:a..:b{.:format}', function(req, res){ var a = parseInt(req.params.a, 10); var b = parseInt(req.params.b, 10); var format = req.params.format; diff --git a/examples/route-separation/index.js b/examples/route-separation/index.js index 5d483811111..0a29c9421a6 100644 --- a/examples/route-separation/index.js +++ b/examples/route-separation/index.js @@ -5,7 +5,7 @@ */ var express = require('../..'); -var path = require('path'); +var path = require('node:path'); var app = express(); var logger = require('morgan'); var cookieParser = require('cookie-parser'); @@ -38,7 +38,7 @@ app.get('/', site.index); // User app.get('/users', user.list); -app.all('/user/:id/:op?', user.load); +app.all('/user/:id{/:op}', user.load); app.get('/user/:id', user.view); app.get('/user/:id/view', user.view); app.get('/user/:id/edit', user.edit); diff --git a/examples/route-separation/user.js b/examples/route-separation/user.js index 1c2aec7cd23..bc6fbd7baf3 100644 --- a/examples/route-separation/user.js +++ b/examples/route-separation/user.js @@ -43,5 +43,5 @@ exports.update = function(req, res){ var user = req.body.user; req.user.name = user.name; req.user.email = user.email; - res.redirect('back'); + res.redirect(req.get('Referrer') || '/'); }; diff --git a/examples/search/index.js b/examples/search/index.js index 0d19444e525..951e0d440a6 100644 --- a/examples/search/index.js +++ b/examples/search/index.js @@ -12,7 +12,7 @@ */ var express = require('../..'); -var path = require('path'); +var path = require('node:path'); var redis = require('redis'); var db = redis.createClient(); @@ -35,10 +35,10 @@ db.sadd('cat', 'luna'); * GET search for :query. */ -app.get('/search/:query?', function(req, res){ +app.get('/search/:query?', function(req, res, next){ var query = req.params.query; db.smembers(query, function(err, vals){ - if (err) return res.send(500); + if (err) return next(err); res.send(vals); }); }); diff --git a/examples/static-files/index.js b/examples/static-files/index.js index 609c546b470..b7c697a2f9f 100644 --- a/examples/static-files/index.js +++ b/examples/static-files/index.js @@ -6,7 +6,7 @@ var express = require('../..'); var logger = require('morgan'); -var path = require('path'); +var path = require('node:path'); var app = express(); // log requests diff --git a/examples/view-constructor/github-view.js b/examples/view-constructor/github-view.js index 43d29336cac..eabfb2d0c18 100644 --- a/examples/view-constructor/github-view.js +++ b/examples/view-constructor/github-view.js @@ -4,8 +4,8 @@ * Module dependencies. */ -var https = require('https'); -var path = require('path'); +var https = require('node:https'); +var path = require('node:path'); var extname = path.extname; /** diff --git a/examples/view-locals/index.js b/examples/view-locals/index.js index a2af24f3553..e6355602d4e 100644 --- a/examples/view-locals/index.js +++ b/examples/view-locals/index.js @@ -5,7 +5,7 @@ */ var express = require('../..'); -var path = require('path'); +var path = require('node:path'); var User = require('./user'); var app = express(); diff --git a/lib/application.js b/lib/application.js index ebb30b51b3d..cf6d78c741e 100644 --- a/lib/application.js +++ b/lib/application.js @@ -14,29 +14,24 @@ */ var finalhandler = require('finalhandler'); -var Router = require('./router'); -var methods = require('methods'); -var middleware = require('./middleware/init'); -var query = require('./middleware/query'); var debug = require('debug')('express:application'); var View = require('./view'); -var http = require('http'); +var http = require('node:http'); +var methods = require('./utils').methods; var compileETag = require('./utils').compileETag; var compileQueryParser = require('./utils').compileQueryParser; var compileTrust = require('./utils').compileTrust; -var deprecate = require('depd')('express'); -var flatten = require('array-flatten'); -var merge = require('utils-merge'); -var resolve = require('path').resolve; -var setPrototypeOf = require('setprototypeof') +var resolve = require('node:path').resolve; +var once = require('once') +var Router = require('router'); /** * Module variables. * @private */ -var hasOwnProperty = Object.prototype.hasOwnProperty var slice = Array.prototype.slice; +var flatten = Array.prototype.flat; /** * Application prototype. @@ -62,11 +57,29 @@ var trustProxyDefaultSymbol = '@@symbol:trust_proxy_default'; */ app.init = function init() { - this.cache = {}; - this.engines = {}; - this.settings = {}; + var router = null; + + this.cache = Object.create(null); + this.engines = Object.create(null); + this.settings = Object.create(null); this.defaultConfiguration(); + + // Setup getting to lazily add base router + Object.defineProperty(this, 'router', { + configurable: true, + enumerable: true, + get: function getrouter() { + if (router === null) { + router = new Router({ + caseSensitive: this.enabled('case sensitive routing'), + strict: this.enabled('strict routing') + }); + } + + return router; + } + }); }; /** @@ -81,7 +94,7 @@ app.defaultConfiguration = function defaultConfiguration() { this.enable('x-powered-by'); this.set('etag', 'weak'); this.set('env', env); - this.set('query parser', 'extended'); + this.set('query parser', 'simple') this.set('subdomain offset', 2); this.set('trust proxy', false); @@ -102,10 +115,10 @@ app.defaultConfiguration = function defaultConfiguration() { } // inherit protos - setPrototypeOf(this.request, parent.request) - setPrototypeOf(this.response, parent.response) - setPrototypeOf(this.engines, parent.engines) - setPrototypeOf(this.settings, parent.settings) + Object.setPrototypeOf(this.request, parent.request) + Object.setPrototypeOf(this.response, parent.response) + Object.setPrototypeOf(this.engines, parent.engines) + Object.setPrototypeOf(this.settings, parent.settings) }); // setup locals @@ -125,32 +138,6 @@ app.defaultConfiguration = function defaultConfiguration() { if (env === 'production') { this.enable('view cache'); } - - Object.defineProperty(this, 'router', { - get: function() { - throw new Error('\'app.router\' is deprecated!\nPlease see the 3.x to 4.x migration guide for details on how to update your app.'); - } - }); -}; - -/** - * lazily adds the base router if it has not yet been added. - * - * We cannot add the base router in the defaultConfiguration because - * it reads app settings which might be set after that has run. - * - * @private - */ -app.lazyrouter = function lazyrouter() { - if (!this._router) { - this._router = new Router({ - caseSensitive: this.enabled('case sensitive routing'), - strict: this.enabled('strict routing') - }); - - this._router.use(query(this.get('query parser fn'))); - this._router.use(middleware.init(this)); - } }; /** @@ -163,22 +150,31 @@ app.lazyrouter = function lazyrouter() { */ app.handle = function handle(req, res, callback) { - var router = this._router; - // final handler var done = callback || finalhandler(req, res, { env: this.get('env'), onerror: logerror.bind(this) }); - // no routes - if (!router) { - debug('no routes defined on app'); - done(); - return; + // set powered by header + if (this.enabled('x-powered-by')) { + res.setHeader('X-Powered-By', 'Express'); + } + + // set circular references + req.res = res; + res.req = req; + + // alter the prototypes + Object.setPrototypeOf(req, this.request) + Object.setPrototypeOf(res, this.response) + + // setup locals + if (!res.locals) { + res.locals = Object.create(null); } - router.handle(req, res, done); + this.router.handle(req, res, done); }; /** @@ -211,15 +207,14 @@ app.use = function use(fn) { } } - var fns = flatten(slice.call(arguments, offset)); + var fns = flatten.call(slice.call(arguments, offset), Infinity); if (fns.length === 0) { throw new TypeError('app.use() requires a middleware function') } - // setup router - this.lazyrouter(); - var router = this._router; + // get router + var router = this.router; fns.forEach(function (fn) { // non-express app @@ -235,8 +230,8 @@ app.use = function use(fn) { router.use(path, function mounted_app(req, res, next) { var orig = req.app; fn.handle(req, res, function (err) { - setPrototypeOf(req, orig.request) - setPrototypeOf(res, orig.response) + Object.setPrototypeOf(req, orig.request) + Object.setPrototypeOf(res, orig.response) next(err); }); }); @@ -259,8 +254,7 @@ app.use = function use(fn) { */ app.route = function route(path) { - this.lazyrouter(); - return this._router.route(path); + return this.router.route(path); }; /** @@ -326,8 +320,6 @@ app.engine = function engine(ext, fn) { */ app.param = function param(name, fn) { - this.lazyrouter(); - if (Array.isArray(name)) { for (var i = 0; i < name.length; i++) { this.param(name[i], fn); @@ -336,7 +328,7 @@ app.param = function param(name, fn) { return this; } - this._router.param(name, fn); + this.router.param(name, fn); return this; }; @@ -359,17 +351,7 @@ app.param = function param(name, fn) { app.set = function set(setting, val) { if (arguments.length === 1) { // app.get(setting) - var settings = this.settings - - while (settings && settings !== Object.prototype) { - if (hasOwnProperty.call(settings, setting)) { - return settings[setting] - } - - settings = Object.getPrototypeOf(settings) - } - - return undefined + return this.settings[setting]; } debug('set "%s" to %o', setting, val); @@ -486,16 +468,14 @@ app.disable = function disable(setting) { * Delegate `.VERB(...)` calls to `router.VERB(...)`. */ -methods.forEach(function(method){ - app[method] = function(path){ +methods.forEach(function (method) { + app[method] = function (path) { if (method === 'get' && arguments.length === 1) { // app.get(setting) return this.set(path); } - this.lazyrouter(); - - var route = this._router.route(path); + var route = this.route(path); route[method].apply(route, slice.call(arguments, 1)); return this; }; @@ -512,9 +492,7 @@ methods.forEach(function(method){ */ app.all = function all(path) { - this.lazyrouter(); - - var route = this._router.route(path); + var route = this.route(path); var args = slice.call(arguments, 1); for (var i = 0; i < methods.length; i++) { @@ -524,10 +502,6 @@ app.all = function all(path) { return this; }; -// del -> delete alias - -app.del = deprecate.function(app.delete, 'app.del: Use app.delete instead'); - /** * Render the given view `name` name with `options` * and a callback accepting an error and the @@ -550,7 +524,6 @@ app.render = function render(name, options, callback) { var done = callback; var engines = this.engines; var opts = options; - var renderOptions = {}; var view; // support callback function as second arg @@ -559,16 +532,8 @@ app.render = function render(name, options, callback) { opts = {}; } - // merge app.locals - merge(renderOptions, this.locals); - - // merge options._locals - if (opts._locals) { - merge(renderOptions, opts._locals); - } - // merge options - merge(renderOptions, opts); + var renderOptions = { ...this.locals, ...opts._locals, ...opts }; // set .cache unless explicitly provided if (renderOptions.cache == null) { @@ -618,8 +583,8 @@ app.render = function render(name, options, callback) { * and HTTPS server you may do so with the "http" * and "https" modules as shown here: * - * var http = require('http') - * , https = require('https') + * var http = require('node:http') + * , https = require('node:https') * , express = require('express') * , app = express(); * @@ -631,9 +596,14 @@ app.render = function render(name, options, callback) { */ app.listen = function listen() { - var server = http.createServer(this); - return server.listen.apply(server, arguments); -}; + var server = http.createServer(this) + var args = Array.prototype.slice.call(arguments) + if (typeof args[args.length - 1] === 'function') { + var done = args[args.length - 1] = once(args[args.length - 1]) + server.once('error', done) + } + return server.listen.apply(server, args) +} /** * Log error using console.error. diff --git a/lib/express.js b/lib/express.js index d188a16db70..2d502eb54e4 100644 --- a/lib/express.js +++ b/lib/express.js @@ -13,11 +13,10 @@ */ var bodyParser = require('body-parser') -var EventEmitter = require('events').EventEmitter; +var EventEmitter = require('node:events').EventEmitter; var mixin = require('merge-descriptors'); var proto = require('./application'); -var Route = require('./router/route'); -var Router = require('./router'); +var Router = require('router'); var req = require('./request'); var res = require('./response'); @@ -68,7 +67,7 @@ exports.response = res; * Expose constructors. */ -exports.Route = Route; +exports.Route = Router.Route; exports.Router = Router; /** @@ -76,41 +75,7 @@ exports.Router = Router; */ exports.json = bodyParser.json -exports.query = require('./middleware/query'); exports.raw = bodyParser.raw exports.static = require('serve-static'); exports.text = bodyParser.text exports.urlencoded = bodyParser.urlencoded - -/** - * Replace removed middleware with an appropriate error message. - */ - -var removedMiddlewares = [ - 'bodyParser', - 'compress', - 'cookieSession', - 'session', - 'logger', - 'cookieParser', - 'favicon', - 'responseTime', - 'errorHandler', - 'timeout', - 'methodOverride', - 'vhost', - 'csrf', - 'directory', - 'limit', - 'multipart', - 'staticCache' -] - -removedMiddlewares.forEach(function (name) { - Object.defineProperty(exports, name, { - get: function () { - throw new Error('Most middleware (like ' + name + ') is no longer bundled with Express and must be installed separately. Please see https://github.com/senchalabs/connect#middleware.'); - }, - configurable: true - }); -}); diff --git a/lib/middleware/init.js b/lib/middleware/init.js deleted file mode 100644 index dfd042747bd..00000000000 --- a/lib/middleware/init.js +++ /dev/null @@ -1,43 +0,0 @@ -/*! - * express - * Copyright(c) 2009-2013 TJ Holowaychuk - * Copyright(c) 2013 Roman Shtylman - * Copyright(c) 2014-2015 Douglas Christopher Wilson - * MIT Licensed - */ - -'use strict'; - -/** - * Module dependencies. - * @private - */ - -var setPrototypeOf = require('setprototypeof') - -/** - * Initialization middleware, exposing the - * request and response to each other, as well - * as defaulting the X-Powered-By header field. - * - * @param {Function} app - * @return {Function} - * @api private - */ - -exports.init = function(app){ - return function expressInit(req, res, next){ - if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express'); - req.res = res; - res.req = req; - req.next = next; - - setPrototypeOf(req, app.request) - setPrototypeOf(res, app.response) - - res.locals = res.locals || Object.create(null); - - next(); - }; -}; - diff --git a/lib/middleware/query.js b/lib/middleware/query.js deleted file mode 100644 index 7e9166947af..00000000000 --- a/lib/middleware/query.js +++ /dev/null @@ -1,47 +0,0 @@ -/*! - * express - * Copyright(c) 2009-2013 TJ Holowaychuk - * Copyright(c) 2013 Roman Shtylman - * Copyright(c) 2014-2015 Douglas Christopher Wilson - * MIT Licensed - */ - -'use strict'; - -/** - * Module dependencies. - */ - -var merge = require('utils-merge') -var parseUrl = require('parseurl'); -var qs = require('qs'); - -/** - * @param {Object} options - * @return {Function} - * @api public - */ - -module.exports = function query(options) { - var opts = merge({}, options) - var queryparse = qs.parse; - - if (typeof options === 'function') { - queryparse = options; - opts = undefined; - } - - if (opts !== undefined && opts.allowPrototypes === undefined) { - // back-compat for qs module - opts.allowPrototypes = true; - } - - return function query(req, res, next){ - if (!req.query) { - var val = parseUrl(req).query; - req.query = queryparse(val, opts); - } - - next(); - }; -}; diff --git a/lib/request.js b/lib/request.js index 3f1eeca6c1a..d8e52630788 100644 --- a/lib/request.js +++ b/lib/request.js @@ -14,10 +14,9 @@ */ var accepts = require('accepts'); -var deprecate = require('depd')('express'); -var isIP = require('net').isIP; +var isIP = require('node:net').isIP; var typeis = require('type-is'); -var http = require('http'); +var http = require('node:http'); var fresh = require('fresh'); var parseRange = require('range-parser'); var parse = require('parseurl'); @@ -147,9 +146,6 @@ req.acceptsEncodings = function(){ return accept.encodings.apply(accept, arguments); }; -req.acceptsEncoding = deprecate.function(req.acceptsEncodings, - 'req.acceptsEncoding: Use acceptsEncodings instead'); - /** * Check if the given `charset`s are acceptable, * otherwise you should respond with 406 "Not Acceptable". @@ -164,9 +160,6 @@ req.acceptsCharsets = function(){ return accept.charsets.apply(accept, arguments); }; -req.acceptsCharset = deprecate.function(req.acceptsCharsets, - 'req.acceptsCharset: Use acceptsCharsets instead'); - /** * Check if the given `lang`s are acceptable, * otherwise you should respond with 406 "Not Acceptable". @@ -181,9 +174,6 @@ req.acceptsLanguages = function(){ return accept.languages.apply(accept, arguments); }; -req.acceptsLanguage = deprecate.function(req.acceptsLanguages, - 'req.acceptsLanguage: Use acceptsLanguages instead'); - /** * Parse Range header field, capping to the given `size`. * @@ -216,38 +206,27 @@ req.range = function range(size, options) { }; /** - * Return the value of param `name` when present or `defaultValue`. - * - * - Checks route placeholders, ex: _/user/:id_ - * - Checks body params, ex: id=12, {"id":12} - * - Checks query string params, ex: ?id=12 + * Parse the query string of `req.url`. * - * To utilize request bodies, `req.body` - * should be an object. This can be done by using - * the `bodyParser()` middleware. + * This uses the "query parser" setting to parse the raw + * string into an object. * - * @param {String} name - * @param {Mixed} [defaultValue] * @return {String} - * @public + * @api public */ -req.param = function param(name, defaultValue) { - var params = this.params || {}; - var body = this.body || {}; - var query = this.query || {}; +defineGetter(req, 'query', function query(){ + var queryparse = this.app.get('query parser fn'); - var args = arguments.length === 1 - ? 'name' - : 'name, default'; - deprecate('req.param(' + args + '): Use req.params, req.body, or req.query instead'); + if (!queryparse) { + // parsing is disabled + return Object.create(null); + } - if (null != params[name] && params.hasOwnProperty(name)) return params[name]; - if (null != body[name]) return body[name]; - if (null != query[name]) return query[name]; + var querystring = parse(this).query; - return defaultValue; -}; + return queryparse(querystring); +}); /** * Check if the incoming request contains the "Content-Type" @@ -414,7 +393,7 @@ defineGetter(req, 'path', function path() { }); /** - * Parse the "Host" header field to a hostname. + * Parse the "Host" header field to a host. * * When the "trust proxy" setting trusts the socket * address, the "X-Forwarded-Host" header field will @@ -424,18 +403,35 @@ defineGetter(req, 'path', function path() { * @public */ -defineGetter(req, 'hostname', function hostname(){ +defineGetter(req, 'host', function host(){ var trust = this.app.get('trust proxy fn'); - var host = this.get('X-Forwarded-Host'); + var val = this.get('X-Forwarded-Host'); - if (!host || !trust(this.connection.remoteAddress, 0)) { - host = this.get('Host'); - } else if (host.indexOf(',') !== -1) { + if (!val || !trust(this.connection.remoteAddress, 0)) { + val = this.get('Host'); + } else if (val.indexOf(',') !== -1) { // Note: X-Forwarded-Host is normally only ever a // single value, but this is to be safe. - host = host.substring(0, host.indexOf(',')).trimRight() + val = val.substring(0, val.indexOf(',')).trimRight() } + return val || undefined; +}); + +/** + * Parse the "Host" header field to a hostname. + * + * When the "trust proxy" setting trusts the socket + * address, the "X-Forwarded-Host" header field will + * be trusted. + * + * @return {String} + * @api public + */ + +defineGetter(req, 'hostname', function hostname(){ + var host = this.host; + if (!host) return; // IPv6 literal support @@ -449,15 +445,9 @@ defineGetter(req, 'hostname', function hostname(){ : host; }); -// TODO: change req.host to return host in next major - -defineGetter(req, 'host', deprecate.function(function host(){ - return this.hostname; -}, 'req.host: Use req.hostname instead')); - /** * Check if the request is fresh, aka - * Last-Modified and/or the ETag + * Last-Modified or the ETag * still match. * * @return {Boolean} diff --git a/lib/response.js b/lib/response.js index dd7b3c8201d..9362d0ed5dd 100644 --- a/lib/response.js +++ b/lib/response.js @@ -12,18 +12,16 @@ * @private */ -var Buffer = require('safe-buffer').Buffer var contentDisposition = require('content-disposition'); var createError = require('http-errors') -var deprecate = require('depd')('express'); var encodeUrl = require('encodeurl'); var escapeHtml = require('escape-html'); -var http = require('http'); -var isAbsolute = require('./utils').isAbsolute; +var http = require('node:http'); var onFinished = require('on-finished'); -var path = require('path'); +var mime = require('mime-types') +var path = require('node:path'); +var pathIsAbsolute = require('node:path').isAbsolute; var statuses = require('statuses') -var merge = require('utils-merge'); var sign = require('cookie-signature').sign; var normalizeType = require('./utils').normalizeType; var normalizeTypes = require('./utils').normalizeTypes; @@ -31,7 +29,6 @@ var setCharset = require('./utils').setCharset; var cookie = require('cookie'); var send = require('send'); var extname = path.extname; -var mime = send.mime; var resolve = path.resolve; var vary = require('vary'); @@ -50,25 +47,28 @@ var res = Object.create(http.ServerResponse.prototype) module.exports = res /** - * Module variables. - * @private - */ - -var charsetRegExp = /;\s*charset\s*=/; -var schemaAndHostRegExp = /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?\/\/[^\\\/\?]+/; - -/** - * Set status `code`. + * Set the HTTP status code for the response. * - * @param {Number} code - * @return {ServerResponse} + * Expects an integer value between 100 and 999 inclusive. + * Throws an error if the provided status code is not an integer or if it's outside the allowable range. + * + * @param {number} code - The HTTP status code to set. + * @return {ServerResponse} - Returns itself for chaining methods. + * @throws {TypeError} If `code` is not an integer. + * @throws {RangeError} If `code` is outside the range 100 to 999. * @public */ res.status = function status(code) { - if ((typeof code === 'string' || Math.floor(code) !== code) && code > 99 && code < 1000) { - deprecate('res.status(' + JSON.stringify(code) + '): use res.status(' + Math.floor(code) + ') instead') + // Check if the status code is not an integer + if (!Number.isInteger(code)) { + throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`); } + // Check if the status code is outside of Node's valid range + if (code < 100 || code > 999) { + throw new RangeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`); + } + this.statusCode = code; return this; }; @@ -80,7 +80,11 @@ res.status = function status(code) { * * res.links({ * next: 'http://api.example.com/users?page=2', - * last: 'http://api.example.com/users?page=5' + * last: 'http://api.example.com/users?page=5', + * pages: [ + * 'http://api.example.com/users?page=1', + * 'http://api.example.com/users?page=2' + * ] * }); * * @param {Object} links @@ -88,11 +92,18 @@ res.status = function status(code) { * @public */ -res.links = function(links){ +res.links = function(links) { var link = this.get('Link') || ''; if (link) link += ', '; - return this.set('Link', link + Object.keys(links).map(function(rel){ - return '<' + links[rel] + '>; rel="' + rel + '"'; + return this.set('Link', link + Object.keys(links).map(function(rel) { + // Allow multiple links if links[rel] is an array + if (Array.isArray(links[rel])) { + return links[rel].map(function (singleLink) { + return `<${singleLink}>; rel="${rel}"`; + }).join(', '); + } else { + return `<${links[rel]}>; rel="${rel}"`; + } }).join(', ')); }; @@ -118,31 +129,6 @@ res.send = function send(body) { // settings var app = this.app; - // allow status / body - if (arguments.length === 2) { - // res.send(body, status) backwards compat - if (typeof arguments[0] !== 'number' && typeof arguments[1] === 'number') { - deprecate('res.send(body, status): Use res.status(status).send(body) instead'); - this.statusCode = arguments[1]; - } else { - deprecate('res.send(status, body): Use res.status(status).send(body) instead'); - this.statusCode = arguments[0]; - chunk = arguments[1]; - } - } - - // disambiguate res.send(status) and res.send(status, num) - if (typeof chunk === 'number' && arguments.length === 1) { - // res.send(status) will set status message as text string - if (!this.get('Content-Type')) { - this.type('txt'); - } - - deprecate('res.send(status): Use res.sendStatus(status) instead'); - this.statusCode = chunk; - chunk = statuses.message[chunk] - } - switch (typeof chunk) { // string defaulting to html case 'string': @@ -155,7 +141,7 @@ res.send = function send(body) { case 'object': if (chunk === null) { chunk = ''; - } else if (Buffer.isBuffer(chunk)) { + } else if (ArrayBuffer.isView(chunk)) { if (!this.get('Content-Type')) { this.type('bin'); } @@ -208,7 +194,7 @@ res.send = function send(body) { } // freshness - if (req.fresh) this.statusCode = 304; + if (req.fresh) this.status(304); // strip irrelevant headers if (204 === this.statusCode || 304 === this.statusCode) { @@ -249,27 +235,12 @@ res.send = function send(body) { */ res.json = function json(obj) { - var val = obj; - - // allow status / body - if (arguments.length === 2) { - // res.json(body, status) backwards compat - if (typeof arguments[1] === 'number') { - deprecate('res.json(obj, status): Use res.status(status).json(obj) instead'); - this.statusCode = arguments[1]; - } else { - deprecate('res.json(status, obj): Use res.status(status).json(obj) instead'); - this.statusCode = arguments[0]; - val = arguments[1]; - } - } - // settings var app = this.app; var escape = app.get('json escape') var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = stringify(val, replacer, spaces, escape) + var body = stringify(obj, replacer, spaces, escape) // content-type if (!this.get('Content-Type')) { @@ -292,27 +263,12 @@ res.json = function json(obj) { */ res.jsonp = function jsonp(obj) { - var val = obj; - - // allow status / body - if (arguments.length === 2) { - // res.jsonp(body, status) backwards compat - if (typeof arguments[1] === 'number') { - deprecate('res.jsonp(obj, status): Use res.status(status).jsonp(obj) instead'); - this.statusCode = arguments[1]; - } else { - deprecate('res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead'); - this.statusCode = arguments[0]; - val = arguments[1]; - } - } - // settings var app = this.app; var escape = app.get('json escape') var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = stringify(val, replacer, spaces, escape) + var body = stringify(obj, replacer, spaces, escape) var callback = this.req.query[app.get('jsonp callback name')]; // content-type @@ -370,7 +326,7 @@ res.jsonp = function jsonp(obj) { res.sendStatus = function sendStatus(statusCode) { var body = statuses.message[statusCode] || String(statusCode) - this.statusCode = statusCode; + this.status(statusCode); this.type('txt'); return this.send(body); @@ -438,82 +394,16 @@ res.sendFile = function sendFile(path, options, callback) { opts = {}; } - if (!opts.root && !isAbsolute(path)) { + if (!opts.root && !pathIsAbsolute(path)) { throw new TypeError('path must be absolute or specify root to res.sendFile'); } // create file stream var pathname = encodeURI(path); - var file = send(req, pathname, opts); - - // transfer - sendfile(res, file, opts, function (err) { - if (done) return done(err); - if (err && err.code === 'EISDIR') return next(); - - // next() all but write errors - if (err && err.code !== 'ECONNABORTED' && err.syscall !== 'write') { - next(err); - } - }); -}; -/** - * Transfer the file at the given `path`. - * - * Automatically sets the _Content-Type_ response header field. - * The callback `callback(err)` is invoked when the transfer is complete - * or when an error occurs. Be sure to check `res.headersSent` - * if you wish to attempt responding, as the header and some data - * may have already been transferred. - * - * Options: - * - * - `maxAge` defaulting to 0 (can be string converted by `ms`) - * - `root` root directory for relative filenames - * - `headers` object of headers to serve with file - * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them - * - * Other options are passed along to `send`. - * - * Examples: - * - * The following example illustrates how `res.sendfile()` may - * be used as an alternative for the `static()` middleware for - * dynamic situations. The code backing `res.sendfile()` is actually - * the same code, so HTTP cache support etc is identical. - * - * app.get('/user/:uid/photos/:file', function(req, res){ - * var uid = req.params.uid - * , file = req.params.file; - * - * req.user.mayViewFilesFrom(uid, function(yes){ - * if (yes) { - * res.sendfile('/uploads/' + uid + '/' + file); - * } else { - * res.send(403, 'Sorry! you cant see that.'); - * } - * }); - * }); - * - * @public - */ - -res.sendfile = function (path, options, callback) { - var done = callback; - var req = this.req; - var res = this; - var next = req.next; - var opts = options || {}; - - // support function as second arg - if (typeof options === 'function') { - done = options; - opts = {}; - } - - // create file stream - var file = send(req, path, opts); + // wire application etag option to send + opts.etag = this.app.enabled('etag'); + var file = send(req, pathname, opts); // transfer sendfile(res, file, opts, function (err) { @@ -527,9 +417,6 @@ res.sendfile = function (path, options, callback) { }); }; -res.sendfile = deprecate.function(res.sendfile, - 'res.sendfile: Use res.sendFile instead'); - /** * Transfer the file at the given `path` as an attachment. * @@ -600,8 +487,10 @@ res.download = function download (path, filename, options, callback) { }; /** - * Set _Content-Type_ response header with `type` through `mime.lookup()` + * Set _Content-Type_ response header with `type` through `mime.contentType()` * when it does not contain "/", or set the Content-Type to `type` otherwise. + * When no mapping is found though `mime.contentType()`, the type is set to + * "application/octet-stream". * * Examples: * @@ -619,7 +508,7 @@ res.download = function download (path, filename, options, callback) { res.contentType = res.type = function contentType(type) { var ct = type.indexOf('/') === -1 - ? mime.lookup(type) + ? (mime.contentType(type) || 'application/octet-stream') : type; return this.set('Content-Type', ct); @@ -768,6 +657,9 @@ res.append = function append(field, val) { * * Aliased as `res.header()`. * + * When the set header is "Content-Type", the type is expanded to include + * the charset if not present using `mime.contentType()`. + * * @param {String|Object} field * @param {String|Array} val * @return {ServerResponse} for chaining @@ -786,10 +678,7 @@ res.header = function header(field, val) { if (Array.isArray(value)) { throw new TypeError('Content-Type cannot be set to an Array'); } - if (!charsetRegExp.test(value)) { - var charset = mime.charsets.lookup(value.split(';')[0]); - if (charset) value += '; charset=' + charset.toLowerCase(); - } + value = mime.contentType(value) } this.setHeader(field, value); @@ -823,7 +712,10 @@ res.get = function(field){ */ res.clearCookie = function clearCookie(name, options) { - var opts = merge({ expires: new Date(1), path: '/' }, options); + // Force cookie expiration by setting expires to the past + const opts = { path: '/', ...options, expires: new Date(1)}; + // ensure maxAge is not passed + delete opts.maxAge return this.cookie(name, '', opts); }; @@ -853,7 +745,7 @@ res.clearCookie = function clearCookie(name, options) { */ res.cookie = function (name, value, options) { - var opts = merge({}, options); + var opts = { ...options }; var secret = this.req.secret; var signed = opts.signed; @@ -905,33 +797,13 @@ res.cookie = function (name, value, options) { */ res.location = function location(url) { - var loc; - - // "back" is an alias for the referrer - if (url === 'back') { - loc = this.req.get('Referrer') || '/'; - } else { - loc = String(url); - } - - var m = schemaAndHostRegExp.exec(loc); - var pos = m ? m[0].length + 1 : 0; - - // Only encode after host to avoid invalid encoding which can introduce - // vulnerabilities (e.g. `\\` to `%5C`). - loc = loc.slice(0, pos) + encodeUrl(loc.slice(pos)); - - return this.set('Location', loc); + return this.set('Location', encodeUrl(url)); }; /** * Redirect to the given `url` with optional response `status` * defaulting to 302. * - * The resulting `url` is determined by `res.location()`, so - * it will play nicely with mounted apps, relative paths, - * `"back"` etc. - * * Examples: * * res.redirect('/foo/bar'); @@ -949,13 +821,8 @@ res.redirect = function redirect(url) { // allow status / url if (arguments.length === 2) { - if (typeof arguments[0] === 'number') { - status = arguments[0]; - address = arguments[1]; - } else { - deprecate('res.redirect(url, status): Use res.redirect(status, url) instead'); - status = arguments[1]; - } + status = arguments[0] + address = arguments[1] } // Set location header @@ -969,7 +836,7 @@ res.redirect = function redirect(url) { html: function(){ var u = escapeHtml(address); - body = '

' + statuses.message[status] + '. Redirecting to ' + u + '

' + body = '

' + statuses.message[status] + '. Redirecting to ' + u + '

' }, default: function(){ @@ -978,7 +845,7 @@ res.redirect = function redirect(url) { }); // Respond - this.statusCode = status; + this.status(status); this.set('Content-Length', Buffer.byteLength(body)); if (this.req.method === 'HEAD') { @@ -998,12 +865,6 @@ res.redirect = function redirect(url) { */ res.vary = function(field){ - // checks for back-compat - if (!field || (Array.isArray(field) && !field.length)) { - deprecate('res.vary(): Provide a field name'); - return this; - } - vary(this, field); return this; diff --git a/lib/router/index.js b/lib/router/index.js deleted file mode 100644 index abb3a6f589e..00000000000 --- a/lib/router/index.js +++ /dev/null @@ -1,673 +0,0 @@ -/*! - * express - * Copyright(c) 2009-2013 TJ Holowaychuk - * Copyright(c) 2013 Roman Shtylman - * Copyright(c) 2014-2015 Douglas Christopher Wilson - * MIT Licensed - */ - -'use strict'; - -/** - * Module dependencies. - * @private - */ - -var Route = require('./route'); -var Layer = require('./layer'); -var methods = require('methods'); -var mixin = require('utils-merge'); -var debug = require('debug')('express:router'); -var deprecate = require('depd')('express'); -var flatten = require('array-flatten'); -var parseUrl = require('parseurl'); -var setPrototypeOf = require('setprototypeof') - -/** - * Module variables. - * @private - */ - -var objectRegExp = /^\[object (\S+)\]$/; -var slice = Array.prototype.slice; -var toString = Object.prototype.toString; - -/** - * Initialize a new `Router` with the given `options`. - * - * @param {Object} [options] - * @return {Router} which is a callable function - * @public - */ - -var proto = module.exports = function(options) { - var opts = options || {}; - - function router(req, res, next) { - router.handle(req, res, next); - } - - // mixin Router class functions - setPrototypeOf(router, proto) - - router.params = {}; - router._params = []; - router.caseSensitive = opts.caseSensitive; - router.mergeParams = opts.mergeParams; - router.strict = opts.strict; - router.stack = []; - - return router; -}; - -/** - * Map the given param placeholder `name`(s) to the given callback. - * - * Parameter mapping is used to provide pre-conditions to routes - * which use normalized placeholders. For example a _:user_id_ parameter - * could automatically load a user's information from the database without - * any additional code, - * - * The callback uses the same signature as middleware, the only difference - * being that the value of the placeholder is passed, in this case the _id_ - * of the user. Once the `next()` function is invoked, just like middleware - * it will continue on to execute the route, or subsequent parameter functions. - * - * Just like in middleware, you must either respond to the request or call next - * to avoid stalling the request. - * - * app.param('user_id', function(req, res, next, id){ - * User.find(id, function(err, user){ - * if (err) { - * return next(err); - * } else if (!user) { - * return next(new Error('failed to load user')); - * } - * req.user = user; - * next(); - * }); - * }); - * - * @param {String} name - * @param {Function} fn - * @return {app} for chaining - * @public - */ - -proto.param = function param(name, fn) { - // param logic - if (typeof name === 'function') { - deprecate('router.param(fn): Refactor to use path params'); - this._params.push(name); - return; - } - - // apply param functions - var params = this._params; - var len = params.length; - var ret; - - if (name[0] === ':') { - deprecate('router.param(' + JSON.stringify(name) + ', fn): Use router.param(' + JSON.stringify(name.slice(1)) + ', fn) instead') - name = name.slice(1) - } - - for (var i = 0; i < len; ++i) { - if (ret = params[i](name, fn)) { - fn = ret; - } - } - - // ensure we end up with a - // middleware function - if ('function' !== typeof fn) { - throw new Error('invalid param() call for ' + name + ', got ' + fn); - } - - (this.params[name] = this.params[name] || []).push(fn); - return this; -}; - -/** - * Dispatch a req, res into the router. - * @private - */ - -proto.handle = function handle(req, res, out) { - var self = this; - - debug('dispatching %s %s', req.method, req.url); - - var idx = 0; - var protohost = getProtohost(req.url) || '' - var removed = ''; - var slashAdded = false; - var sync = 0 - var paramcalled = {}; - - // store options for OPTIONS request - // only used if OPTIONS request - var options = []; - - // middleware and routes - var stack = self.stack; - - // manage inter-router variables - var parentParams = req.params; - var parentUrl = req.baseUrl || ''; - var done = restore(out, req, 'baseUrl', 'next', 'params'); - - // setup next layer - req.next = next; - - // for options requests, respond with a default if nothing else responds - if (req.method === 'OPTIONS') { - done = wrap(done, function(old, err) { - if (err || options.length === 0) return old(err); - sendOptionsResponse(res, options, old); - }); - } - - // setup basic req values - req.baseUrl = parentUrl; - req.originalUrl = req.originalUrl || req.url; - - next(); - - function next(err) { - var layerError = err === 'route' - ? null - : err; - - // remove added slash - if (slashAdded) { - req.url = req.url.slice(1) - slashAdded = false; - } - - // restore altered req.url - if (removed.length !== 0) { - req.baseUrl = parentUrl; - req.url = protohost + removed + req.url.slice(protohost.length) - removed = ''; - } - - // signal to exit router - if (layerError === 'router') { - setImmediate(done, null) - return - } - - // no more matching layers - if (idx >= stack.length) { - setImmediate(done, layerError); - return; - } - - // max sync stack - if (++sync > 100) { - return setImmediate(next, err) - } - - // get pathname of request - var path = getPathname(req); - - if (path == null) { - return done(layerError); - } - - // find next matching layer - var layer; - var match; - var route; - - while (match !== true && idx < stack.length) { - layer = stack[idx++]; - match = matchLayer(layer, path); - route = layer.route; - - if (typeof match !== 'boolean') { - // hold on to layerError - layerError = layerError || match; - } - - if (match !== true) { - continue; - } - - if (!route) { - // process non-route handlers normally - continue; - } - - if (layerError) { - // routes do not match with a pending error - match = false; - continue; - } - - var method = req.method; - var has_method = route._handles_method(method); - - // build up automatic options response - if (!has_method && method === 'OPTIONS') { - appendMethods(options, route._options()); - } - - // don't even bother matching route - if (!has_method && method !== 'HEAD') { - match = false; - } - } - - // no match - if (match !== true) { - return done(layerError); - } - - // store route for dispatch on change - if (route) { - req.route = route; - } - - // Capture one-time layer values - req.params = self.mergeParams - ? mergeParams(layer.params, parentParams) - : layer.params; - var layerPath = layer.path; - - // this should be done for the layer - self.process_params(layer, paramcalled, req, res, function (err) { - if (err) { - next(layerError || err) - } else if (route) { - layer.handle_request(req, res, next) - } else { - trim_prefix(layer, layerError, layerPath, path) - } - - sync = 0 - }); - } - - function trim_prefix(layer, layerError, layerPath, path) { - if (layerPath.length !== 0) { - // Validate path is a prefix match - if (layerPath !== path.slice(0, layerPath.length)) { - next(layerError) - return - } - - // Validate path breaks on a path separator - var c = path[layerPath.length] - if (c && c !== '/' && c !== '.') return next(layerError) - - // Trim off the part of the url that matches the route - // middleware (.use stuff) needs to have the path stripped - debug('trim prefix (%s) from url %s', layerPath, req.url); - removed = layerPath; - req.url = protohost + req.url.slice(protohost.length + removed.length) - - // Ensure leading slash - if (!protohost && req.url[0] !== '/') { - req.url = '/' + req.url; - slashAdded = true; - } - - // Setup base URL (no trailing slash) - req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' - ? removed.substring(0, removed.length - 1) - : removed); - } - - debug('%s %s : %s', layer.name, layerPath, req.originalUrl); - - if (layerError) { - layer.handle_error(layerError, req, res, next); - } else { - layer.handle_request(req, res, next); - } - } -}; - -/** - * Process any parameters for the layer. - * @private - */ - -proto.process_params = function process_params(layer, called, req, res, done) { - var params = this.params; - - // captured parameters from the layer, keys and values - var keys = layer.keys; - - // fast track - if (!keys || keys.length === 0) { - return done(); - } - - var i = 0; - var name; - var paramIndex = 0; - var key; - var paramVal; - var paramCallbacks; - var paramCalled; - - // process params in order - // param callbacks can be async - function param(err) { - if (err) { - return done(err); - } - - if (i >= keys.length ) { - return done(); - } - - paramIndex = 0; - key = keys[i++]; - name = key.name; - paramVal = req.params[name]; - paramCallbacks = params[name]; - paramCalled = called[name]; - - if (paramVal === undefined || !paramCallbacks) { - return param(); - } - - // param previously called with same value or error occurred - if (paramCalled && (paramCalled.match === paramVal - || (paramCalled.error && paramCalled.error !== 'route'))) { - // restore value - req.params[name] = paramCalled.value; - - // next param - return param(paramCalled.error); - } - - called[name] = paramCalled = { - error: null, - match: paramVal, - value: paramVal - }; - - paramCallback(); - } - - // single param callbacks - function paramCallback(err) { - var fn = paramCallbacks[paramIndex++]; - - // store updated value - paramCalled.value = req.params[key.name]; - - if (err) { - // store error - paramCalled.error = err; - param(err); - return; - } - - if (!fn) return param(); - - try { - fn(req, res, paramCallback, paramVal, key.name); - } catch (e) { - paramCallback(e); - } - } - - param(); -}; - -/** - * Use the given middleware function, with optional path, defaulting to "/". - * - * Use (like `.all`) will run for any http METHOD, but it will not add - * handlers for those methods so OPTIONS requests will not consider `.use` - * functions even if they could respond. - * - * The other difference is that _route_ path is stripped and not visible - * to the handler function. The main effect of this feature is that mounted - * handlers can operate without any code changes regardless of the "prefix" - * pathname. - * - * @public - */ - -proto.use = function use(fn) { - var offset = 0; - var path = '/'; - - // default path to '/' - // disambiguate router.use([fn]) - if (typeof fn !== 'function') { - var arg = fn; - - while (Array.isArray(arg) && arg.length !== 0) { - arg = arg[0]; - } - - // first arg is the path - if (typeof arg !== 'function') { - offset = 1; - path = fn; - } - } - - var callbacks = flatten(slice.call(arguments, offset)); - - if (callbacks.length === 0) { - throw new TypeError('Router.use() requires a middleware function') - } - - for (var i = 0; i < callbacks.length; i++) { - var fn = callbacks[i]; - - if (typeof fn !== 'function') { - throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn)) - } - - // add the middleware - debug('use %o %s', path, fn.name || '') - - var layer = new Layer(path, { - sensitive: this.caseSensitive, - strict: false, - end: false - }, fn); - - layer.route = undefined; - - this.stack.push(layer); - } - - return this; -}; - -/** - * Create a new Route for the given path. - * - * Each route contains a separate middleware stack and VERB handlers. - * - * See the Route api documentation for details on adding handlers - * and middleware to routes. - * - * @param {String} path - * @return {Route} - * @public - */ - -proto.route = function route(path) { - var route = new Route(path); - - var layer = new Layer(path, { - sensitive: this.caseSensitive, - strict: this.strict, - end: true - }, route.dispatch.bind(route)); - - layer.route = route; - - this.stack.push(layer); - return route; -}; - -// create Router#VERB functions -methods.concat('all').forEach(function(method){ - proto[method] = function(path){ - var route = this.route(path) - route[method].apply(route, slice.call(arguments, 1)); - return this; - }; -}); - -// append methods to a list of methods -function appendMethods(list, addition) { - for (var i = 0; i < addition.length; i++) { - var method = addition[i]; - if (list.indexOf(method) === -1) { - list.push(method); - } - } -} - -// get pathname of request -function getPathname(req) { - try { - return parseUrl(req).pathname; - } catch (err) { - return undefined; - } -} - -// Get get protocol + host for a URL -function getProtohost(url) { - if (typeof url !== 'string' || url.length === 0 || url[0] === '/') { - return undefined - } - - var searchIndex = url.indexOf('?') - var pathLength = searchIndex !== -1 - ? searchIndex - : url.length - var fqdnIndex = url.slice(0, pathLength).indexOf('://') - - return fqdnIndex !== -1 - ? url.substring(0, url.indexOf('/', 3 + fqdnIndex)) - : undefined -} - -// get type for error message -function gettype(obj) { - var type = typeof obj; - - if (type !== 'object') { - return type; - } - - // inspect [[Class]] for objects - return toString.call(obj) - .replace(objectRegExp, '$1'); -} - -/** - * Match path to a layer. - * - * @param {Layer} layer - * @param {string} path - * @private - */ - -function matchLayer(layer, path) { - try { - return layer.match(path); - } catch (err) { - return err; - } -} - -// merge params with parent params -function mergeParams(params, parent) { - if (typeof parent !== 'object' || !parent) { - return params; - } - - // make copy of parent for base - var obj = mixin({}, parent); - - // simple non-numeric merging - if (!(0 in params) || !(0 in parent)) { - return mixin(obj, params); - } - - var i = 0; - var o = 0; - - // determine numeric gaps - while (i in params) { - i++; - } - - while (o in parent) { - o++; - } - - // offset numeric indices in params before merge - for (i--; i >= 0; i--) { - params[i + o] = params[i]; - - // create holes for the merge when necessary - if (i < o) { - delete params[i]; - } - } - - return mixin(obj, params); -} - -// restore obj props after function -function restore(fn, obj) { - var props = new Array(arguments.length - 2); - var vals = new Array(arguments.length - 2); - - for (var i = 0; i < props.length; i++) { - props[i] = arguments[i + 2]; - vals[i] = obj[props[i]]; - } - - return function () { - // restore vals - for (var i = 0; i < props.length; i++) { - obj[props[i]] = vals[i]; - } - - return fn.apply(this, arguments); - }; -} - -// send an OPTIONS response -function sendOptionsResponse(res, options, next) { - try { - var body = options.join(','); - res.set('Allow', body); - res.send(body); - } catch (err) { - next(err); - } -} - -// wrap a function -function wrap(old, fn) { - return function proxy() { - var args = new Array(arguments.length + 1); - - args[0] = old; - for (var i = 0, len = arguments.length; i < len; i++) { - args[i + 1] = arguments[i]; - } - - fn.apply(this, args); - }; -} diff --git a/lib/router/layer.js b/lib/router/layer.js deleted file mode 100644 index 4dc8e86d4f7..00000000000 --- a/lib/router/layer.js +++ /dev/null @@ -1,181 +0,0 @@ -/*! - * express - * Copyright(c) 2009-2013 TJ Holowaychuk - * Copyright(c) 2013 Roman Shtylman - * Copyright(c) 2014-2015 Douglas Christopher Wilson - * MIT Licensed - */ - -'use strict'; - -/** - * Module dependencies. - * @private - */ - -var pathRegexp = require('path-to-regexp'); -var debug = require('debug')('express:router:layer'); - -/** - * Module variables. - * @private - */ - -var hasOwnProperty = Object.prototype.hasOwnProperty; - -/** - * Module exports. - * @public - */ - -module.exports = Layer; - -function Layer(path, options, fn) { - if (!(this instanceof Layer)) { - return new Layer(path, options, fn); - } - - debug('new %o', path) - var opts = options || {}; - - this.handle = fn; - this.name = fn.name || ''; - this.params = undefined; - this.path = undefined; - this.regexp = pathRegexp(path, this.keys = [], opts); - - // set fast path flags - this.regexp.fast_star = path === '*' - this.regexp.fast_slash = path === '/' && opts.end === false -} - -/** - * Handle the error for the layer. - * - * @param {Error} error - * @param {Request} req - * @param {Response} res - * @param {function} next - * @api private - */ - -Layer.prototype.handle_error = function handle_error(error, req, res, next) { - var fn = this.handle; - - if (fn.length !== 4) { - // not a standard error handler - return next(error); - } - - try { - fn(error, req, res, next); - } catch (err) { - next(err); - } -}; - -/** - * Handle the request for the layer. - * - * @param {Request} req - * @param {Response} res - * @param {function} next - * @api private - */ - -Layer.prototype.handle_request = function handle(req, res, next) { - var fn = this.handle; - - if (fn.length > 3) { - // not a standard request handler - return next(); - } - - try { - fn(req, res, next); - } catch (err) { - next(err); - } -}; - -/** - * Check if this route matches `path`, if so - * populate `.params`. - * - * @param {String} path - * @return {Boolean} - * @api private - */ - -Layer.prototype.match = function match(path) { - var match - - if (path != null) { - // fast path non-ending match for / (any path matches) - if (this.regexp.fast_slash) { - this.params = {} - this.path = '' - return true - } - - // fast path for * (everything matched in a param) - if (this.regexp.fast_star) { - this.params = {'0': decode_param(path)} - this.path = path - return true - } - - // match the path - match = this.regexp.exec(path) - } - - if (!match) { - this.params = undefined; - this.path = undefined; - return false; - } - - // store values - this.params = {}; - this.path = match[0] - - var keys = this.keys; - var params = this.params; - - for (var i = 1; i < match.length; i++) { - var key = keys[i - 1]; - var prop = key.name; - var val = decode_param(match[i]) - - if (val !== undefined || !(hasOwnProperty.call(params, prop))) { - params[prop] = val; - } - } - - return true; -}; - -/** - * Decode param value. - * - * @param {string} val - * @return {string} - * @private - */ - -function decode_param(val) { - if (typeof val !== 'string' || val.length === 0) { - return val; - } - - try { - return decodeURIComponent(val); - } catch (err) { - if (err instanceof URIError) { - err.message = 'Failed to decode param \'' + val + '\''; - err.status = err.statusCode = 400; - } - - throw err; - } -} diff --git a/lib/router/route.js b/lib/router/route.js deleted file mode 100644 index a65756d6de6..00000000000 --- a/lib/router/route.js +++ /dev/null @@ -1,230 +0,0 @@ -/*! - * express - * Copyright(c) 2009-2013 TJ Holowaychuk - * Copyright(c) 2013 Roman Shtylman - * Copyright(c) 2014-2015 Douglas Christopher Wilson - * MIT Licensed - */ - -'use strict'; - -/** - * Module dependencies. - * @private - */ - -var debug = require('debug')('express:router:route'); -var flatten = require('array-flatten'); -var Layer = require('./layer'); -var methods = require('methods'); - -/** - * Module variables. - * @private - */ - -var slice = Array.prototype.slice; -var toString = Object.prototype.toString; - -/** - * Module exports. - * @public - */ - -module.exports = Route; - -/** - * Initialize `Route` with the given `path`, - * - * @param {String} path - * @public - */ - -function Route(path) { - this.path = path; - this.stack = []; - - debug('new %o', path) - - // route handlers for various http methods - this.methods = {}; -} - -/** - * Determine if the route handles a given method. - * @private - */ - -Route.prototype._handles_method = function _handles_method(method) { - if (this.methods._all) { - return true; - } - - // normalize name - var name = typeof method === 'string' - ? method.toLowerCase() - : method - - if (name === 'head' && !this.methods['head']) { - name = 'get'; - } - - return Boolean(this.methods[name]); -}; - -/** - * @return {Array} supported HTTP methods - * @private - */ - -Route.prototype._options = function _options() { - var methods = Object.keys(this.methods); - - // append automatic head - if (this.methods.get && !this.methods.head) { - methods.push('head'); - } - - for (var i = 0; i < methods.length; i++) { - // make upper case - methods[i] = methods[i].toUpperCase(); - } - - return methods; -}; - -/** - * dispatch req, res into this route - * @private - */ - -Route.prototype.dispatch = function dispatch(req, res, done) { - var idx = 0; - var stack = this.stack; - var sync = 0 - - if (stack.length === 0) { - return done(); - } - var method = typeof req.method === 'string' - ? req.method.toLowerCase() - : req.method - - if (method === 'head' && !this.methods['head']) { - method = 'get'; - } - - req.route = this; - - next(); - - function next(err) { - // signal to exit route - if (err && err === 'route') { - return done(); - } - - // signal to exit router - if (err && err === 'router') { - return done(err) - } - - // max sync stack - if (++sync > 100) { - return setImmediate(next, err) - } - - var layer = stack[idx++] - - // end of layers - if (!layer) { - return done(err) - } - - if (layer.method && layer.method !== method) { - next(err) - } else if (err) { - layer.handle_error(err, req, res, next); - } else { - layer.handle_request(req, res, next); - } - - sync = 0 - } -}; - -/** - * Add a handler for all HTTP verbs to this route. - * - * Behaves just like middleware and can respond or call `next` - * to continue processing. - * - * You can use multiple `.all` call to add multiple handlers. - * - * function check_something(req, res, next){ - * next(); - * }; - * - * function validate_user(req, res, next){ - * next(); - * }; - * - * route - * .all(validate_user) - * .all(check_something) - * .get(function(req, res, next){ - * res.send('hello world'); - * }); - * - * @param {function} handler - * @return {Route} for chaining - * @api public - */ - -Route.prototype.all = function all() { - var handles = flatten(slice.call(arguments)); - - for (var i = 0; i < handles.length; i++) { - var handle = handles[i]; - - if (typeof handle !== 'function') { - var type = toString.call(handle); - var msg = 'Route.all() requires a callback function but got a ' + type - throw new TypeError(msg); - } - - var layer = Layer('/', {}, handle); - layer.method = undefined; - - this.methods._all = true; - this.stack.push(layer); - } - - return this; -}; - -methods.forEach(function(method){ - Route.prototype[method] = function(){ - var handles = flatten(slice.call(arguments)); - - for (var i = 0; i < handles.length; i++) { - var handle = handles[i]; - - if (typeof handle !== 'function') { - var type = toString.call(handle); - var msg = 'Route.' + method + '() requires a callback function but got a ' + type - throw new Error(msg); - } - - debug('%s %o', method, this.path) - - var layer = Layer('/', {}, handle); - layer.method = method; - - this.methods[method] = true; - this.stack.push(layer); - } - - return this; - }; -}); diff --git a/lib/utils.js b/lib/utils.js index 56e12b9b541..d53c5a13374 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -12,17 +12,20 @@ * @api private */ -var Buffer = require('safe-buffer').Buffer -var contentDisposition = require('content-disposition'); +var { METHODS } = require('node:http'); var contentType = require('content-type'); -var deprecate = require('depd')('express'); -var flatten = require('array-flatten'); -var mime = require('send').mime; var etag = require('etag'); +var mime = require('mime-types') var proxyaddr = require('proxy-addr'); var qs = require('qs'); var querystring = require('querystring'); +/** + * A list of lowercased HTTP methods that are supported by Node.js. + * @api private + */ +exports.methods = METHODS.map((method) => method.toLowerCase()); + /** * Return strong ETag for `body`. * @@ -45,31 +48,6 @@ exports.etag = createETagGenerator({ weak: false }) exports.wetag = createETagGenerator({ weak: true }) -/** - * Check if `path` looks absolute. - * - * @param {String} path - * @return {Boolean} - * @api private - */ - -exports.isAbsolute = function(path){ - if ('/' === path[0]) return true; - if (':' === path[1] && ('\\' === path[2] || '/' === path[2])) return true; // Windows device path - if ('\\\\' === path.substring(0, 2)) return true; // Microsoft Azure absolute path -}; - -/** - * Flatten the given `arr`. - * - * @param {Array} arr - * @return {Array} - * @api private - */ - -exports.flatten = deprecate.function(flatten, - 'utils.flatten: use array-flatten npm module instead'); - /** * Normalize the given `type`, for example "html" becomes "text/html". * @@ -81,7 +59,7 @@ exports.flatten = deprecate.function(flatten, exports.normalizeType = function(type){ return ~type.indexOf('/') ? acceptParams(type) - : { value: mime.lookup(type), params: {} }; + : { value: (mime.lookup(type) || 'application/octet-stream'), params: {} } }; /** @@ -92,27 +70,10 @@ exports.normalizeType = function(type){ * @api private */ -exports.normalizeTypes = function(types){ - var ret = []; - - for (var i = 0; i < types.length; ++i) { - ret.push(exports.normalizeType(types[i])); - } - - return ret; +exports.normalizeTypes = function(types) { + return types.map(exports.normalizeType); }; -/** - * Generate Content-Disposition header appropriate for the filename. - * non-ascii filenames are urlencoded and a filename* parameter is added - * - * @param {String} filename - * @return {String} - * @api private - */ - -exports.contentDisposition = deprecate.function(contentDisposition, - 'utils.contentDisposition: use content-disposition npm module instead'); /** * Parse accept params `str` returning an @@ -124,16 +85,33 @@ exports.contentDisposition = deprecate.function(contentDisposition, */ function acceptParams (str) { - var parts = str.split(/ *; */); - var ret = { value: parts[0], quality: 1, params: {} } + var length = str.length; + var colonIndex = str.indexOf(';'); + var index = colonIndex === -1 ? length : colonIndex; + var ret = { value: str.slice(0, index).trim(), quality: 1, params: {} }; + + while (index < length) { + var splitIndex = str.indexOf('=', index); + if (splitIndex === -1) break; + + var colonIndex = str.indexOf(';', index); + var endIndex = colonIndex === -1 ? length : colonIndex; + + if (splitIndex > endIndex) { + index = str.lastIndexOf(';', splitIndex - 1) + 1; + continue; + } - for (var i = 1; i < parts.length; ++i) { - var pms = parts[i].split(/ *= */); - if ('q' === pms[0]) { - ret.quality = parseFloat(pms[1]); + var key = str.slice(index, splitIndex).trim(); + var value = str.slice(splitIndex + 1, endIndex).trim(); + + if (key === 'q') { + ret.quality = parseFloat(value); } else { - ret.params[pms[0]] = pms[1]; + ret.params[key] = value; } + + index = endIndex + 1; } return ret; @@ -192,7 +170,6 @@ exports.compileQueryParser = function compileQueryParser(val) { fn = querystring.parse; break; case false: - fn = newObject; break; case 'extended': fn = parseExtendedQueryString; @@ -290,14 +267,3 @@ function parseExtendedQueryString(str) { allowPrototypes: true }); } - -/** - * Return new empty object. - * - * @return {Object} - * @api private - */ - -function newObject() { - return {}; -} diff --git a/lib/view.js b/lib/view.js index c08ab4d8d52..d66b4a2d89c 100644 --- a/lib/view.js +++ b/lib/view.js @@ -14,8 +14,8 @@ */ var debug = require('debug')('express:view'); -var path = require('path'); -var fs = require('fs'); +var path = require('node:path'); +var fs = require('node:fs'); /** * Module variables. @@ -131,8 +131,31 @@ View.prototype.lookup = function lookup(name) { */ View.prototype.render = function render(options, callback) { + var sync = true; + debug('render "%s"', this.path); - this.engine(this.path, options, callback); + + // render, normalizing sync callbacks + this.engine(this.path, options, function onRender() { + if (!sync) { + return callback.apply(this, arguments); + } + + // copy arguments + var args = new Array(arguments.length); + var cntx = this; + + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } + + // force callback to be async + return process.nextTick(function renderTick() { + return callback.apply(cntx, args); + }); + }); + + sync = false; }; /** diff --git a/package.json b/package.json index f299d882b0d..bdcd25e60fa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "express", "description": "Fast, unopinionated, minimalist web framework", - "version": "4.19.2", + "version": "5.1.0", "author": "TJ Holowaychuk ", "contributors": [ "Aaron Heckmann ", @@ -14,7 +14,11 @@ ], "license": "MIT", "repository": "expressjs/express", - "homepage": "http://expressjs.com/", + "homepage": "https://expressjs.com/", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + }, "keywords": [ "express", "framework", @@ -28,58 +32,54 @@ "api" ], "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "devDependencies": { "after": "0.8.2", - "connect-redis": "3.4.2", - "cookie-parser": "1.4.6", - "cookie-session": "2.0.0", - "ejs": "3.1.9", + "connect-redis": "^8.0.1", + "cookie-parser": "1.4.7", + "cookie-session": "2.1.0", + "ejs": "^3.1.10", "eslint": "8.47.0", - "express-session": "1.17.2", + "express-session": "^1.18.1", "hbs": "4.2.0", - "marked": "0.7.0", + "marked": "^15.0.3", "method-override": "3.0.0", - "mocha": "10.2.0", + "mocha": "^10.7.3", "morgan": "1.10.0", - "nyc": "15.1.0", + "nyc": "^17.1.0", "pbkdf2-password": "1.2.1", - "supertest": "6.3.0", + "supertest": "^6.3.0", "vhost": "~3.0.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" }, "files": [ "LICENSE", @@ -90,9 +90,9 @@ ], "scripts": { "lint": "eslint .", - "test": "mocha --require test/support/env --reporter spec --bail --check-leaks test/ test/acceptance/", - "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", - "test-cov": "nyc --reporter=html --reporter=text npm test", + "test": "mocha --require test/support/env --reporter spec --check-leaks test/ test/acceptance/", + "test-ci": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=html --reporter=text npm test", "test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/" } } diff --git a/test/Route.js b/test/Route.js index 2a37b9a4839..e4b73c7e6ec 100644 --- a/test/Route.js +++ b/test/Route.js @@ -1,10 +1,10 @@ 'use strict' var after = require('after'); -var assert = require('assert') +var assert = require('node:assert') var express = require('../') , Route = express.Route - , methods = require('methods') + , methods = require('../lib/utils').methods describe('Route', function(){ it('should work without handlers', function(done) { diff --git a/test/Router.js b/test/Router.js index b22001a9ff2..7bac7159b04 100644 --- a/test/Router.js +++ b/test/Router.js @@ -3,11 +3,11 @@ var after = require('after'); var express = require('../') , Router = express.Router - , methods = require('methods') - , assert = require('assert'); + , methods = require('../lib/utils').methods + , assert = require('node:assert'); -describe('Router', function(){ - it('should return a function with router methods', function() { +describe('Router', function () { + it('should return a function with router methods', function () { var router = new Router(); assert(typeof router === 'function') @@ -16,32 +16,32 @@ describe('Router', function(){ assert(typeof router.use === 'function') }); - it('should support .use of other routers', function(done){ + it('should support .use of other routers', function (done) { var router = new Router(); var another = new Router(); - another.get('/bar', function(req, res){ + another.get('/bar', function (req, res) { res.end(); }); router.use('/foo', another); - router.handle({ url: '/foo/bar', method: 'GET' }, { end: done }); + router.handle({ url: '/foo/bar', method: 'GET' }, { end: done }, function () { }); }); - it('should support dynamic routes', function(done){ + it('should support dynamic routes', function (done) { var router = new Router(); var another = new Router(); - another.get('/:bar', function(req, res){ + another.get('/:bar', function (req, res) { assert.strictEqual(req.params.bar, 'route') res.end(); }); router.use('/:foo', another); - router.handle({ url: '/test/route', method: 'GET' }, { end: done }); + router.handle({ url: '/test/route', method: 'GET' }, { end: done }, function () { }); }); - it('should handle blank URL', function(done){ + it('should handle blank URL', function (done) { var router = new Router(); router.use(function (req, res) { @@ -88,10 +88,10 @@ describe('Router', function(){ }) }) - it('should not stack overflow with many registered routes', function(done){ + it('should not stack overflow with many registered routes', function (done) { this.timeout(5000) // long-running test - var handler = function(req, res){ res.end(new Error('wrong handler')) }; + var handler = function (req, res) { res.end(new Error('wrong handler')) }; var router = new Router(); for (var i = 0; i < 6000; i++) { @@ -102,7 +102,7 @@ describe('Router', function(){ res.end(); }); - router.handle({ url: '/', method: 'GET' }, { end: done }); + router.handle({ url: '/', method: 'GET' }, { end: done }, function () { }); }); it('should not stack overflow with a large sync route stack', function (done) { @@ -127,7 +127,9 @@ describe('Router', function(){ res.end() }) - router.handle({ url: '/foo', method: 'GET' }, { end: done }) + router.handle({ url: '/foo', method: 'GET' }, { end: done }, function (err) { + assert(!err, err); + }); }) it('should not stack overflow with a large sync middleware stack', function (done) { @@ -152,72 +154,74 @@ describe('Router', function(){ res.end() }) - router.handle({ url: '/', method: 'GET' }, { end: done }) + router.handle({ url: '/', method: 'GET' }, { end: done }, function (err) { + assert(!err, err); + }) }) - describe('.handle', function(){ - it('should dispatch', function(done){ + describe('.handle', function () { + it('should dispatch', function (done) { var router = new Router(); - router.route('/foo').get(function(req, res){ + router.route('/foo').get(function (req, res) { res.send('foo'); }); var res = { - send: function(val) { + send: function (val) { assert.strictEqual(val, 'foo') done(); } } - router.handle({ url: '/foo', method: 'GET' }, res); + router.handle({ url: '/foo', method: 'GET' }, res, function () { }); }) }) - describe('.multiple callbacks', function(){ - it('should throw if a callback is null', function(){ + describe('.multiple callbacks', function () { + it('should throw if a callback is null', function () { assert.throws(function () { var router = new Router(); router.route('/foo').all(null); }) }) - it('should throw if a callback is undefined', function(){ + it('should throw if a callback is undefined', function () { assert.throws(function () { var router = new Router(); router.route('/foo').all(undefined); }) }) - it('should throw if a callback is not a function', function(){ + it('should throw if a callback is not a function', function () { assert.throws(function () { var router = new Router(); router.route('/foo').all('not a function'); }) }) - it('should not throw if all callbacks are functions', function(){ + it('should not throw if all callbacks are functions', function () { var router = new Router(); - router.route('/foo').all(function(){}).all(function(){}); + router.route('/foo').all(function () { }).all(function () { }); }) }) - describe('error', function(){ - it('should skip non error middleware', function(done){ + describe('error', function () { + it('should skip non error middleware', function (done) { var router = new Router(); - router.get('/foo', function(req, res, next){ + router.get('/foo', function (req, res, next) { next(new Error('foo')); }); - router.get('/bar', function(req, res, next){ + router.get('/bar', function (req, res, next) { next(new Error('bar')); }); - router.use(function(req, res, next){ + router.use(function (req, res, next) { assert(false); }); - router.use(function(err, req, res, next){ + router.use(function (err, req, res, next) { assert.equal(err.message, 'foo'); done(); }); @@ -225,59 +229,59 @@ describe('Router', function(){ router.handle({ url: '/foo', method: 'GET' }, {}, done); }); - it('should handle throwing inside routes with params', function(done) { + it('should handle throwing inside routes with params', function (done) { var router = new Router(); router.get('/foo/:id', function () { throw new Error('foo'); }); - router.use(function(req, res, next){ + router.use(function (req, res, next) { assert(false); }); - router.use(function(err, req, res, next){ + router.use(function (err, req, res, next) { assert.equal(err.message, 'foo'); done(); }); - router.handle({ url: '/foo/2', method: 'GET' }, {}, function() {}); + router.handle({ url: '/foo/2', method: 'GET' }, {}, function () { }); }); - it('should handle throwing in handler after async param', function(done) { + it('should handle throwing in handler after async param', function (done) { var router = new Router(); - router.param('user', function(req, res, next, val){ - process.nextTick(function(){ + router.param('user', function (req, res, next, val) { + process.nextTick(function () { req.user = val; next(); }); }); - router.use('/:user', function(req, res, next){ + router.use('/:user', function (req, res, next) { throw new Error('oh no!'); }); - router.use(function(err, req, res, next){ + router.use(function (err, req, res, next) { assert.equal(err.message, 'oh no!'); done(); }); - router.handle({ url: '/bob', method: 'GET' }, {}, function() {}); + router.handle({ url: '/bob', method: 'GET' }, {}, function () { }); }); - it('should handle throwing inside error handlers', function(done) { + it('should handle throwing inside error handlers', function (done) { var router = new Router(); - router.use(function(req, res, next){ + router.use(function (req, res, next) { throw new Error('boom!'); }); - router.use(function(err, req, res, next){ + router.use(function (err, req, res, next) { throw new Error('oops'); }); - router.use(function(err, req, res, next){ + router.use(function (err, req, res, next) { assert.equal(err.message, 'oops'); done(); }); @@ -408,73 +412,55 @@ describe('Router', function(){ }); }) - describe('.all', function() { - it('should support using .all to capture all http verbs', function(done){ + describe('.all', function () { + it('should support using .all to capture all http verbs', function (done) { var router = new Router(); var count = 0; - router.all('/foo', function(){ count++; }); + router.all('/foo', function () { count++; }); var url = '/foo?bar=baz'; methods.forEach(function testMethod(method) { - router.handle({ url: url, method: method }, {}, function() {}); + router.handle({ url: url, method: method }, {}, function () { }); }); assert.equal(count, methods.length); done(); }) - - it('should be called for any URL when "*"', function (done) { - var cb = after(4, done) - var router = new Router() - - function no () { - throw new Error('should not be called') - } - - router.all('*', function (req, res) { - res.end() - }) - - router.handle({ url: '/', method: 'GET' }, { end: cb }, no) - router.handle({ url: '/foo', method: 'GET' }, { end: cb }, no) - router.handle({ url: 'foo', method: 'GET' }, { end: cb }, no) - router.handle({ url: '*', method: 'GET' }, { end: cb }, no) - }) }) - describe('.use', function() { + describe('.use', function () { it('should require middleware', function () { var router = new Router() - assert.throws(function () { router.use('/') }, /requires a middleware function/) + assert.throws(function () { router.use('/') }, /argument handler is required/) }) it('should reject string as middleware', function () { var router = new Router() - assert.throws(function () { router.use('/', 'foo') }, /requires a middleware function but got a string/) + assert.throws(function () { router.use('/', 'foo') }, /argument handler must be a function/) }) it('should reject number as middleware', function () { var router = new Router() - assert.throws(function () { router.use('/', 42) }, /requires a middleware function but got a number/) + assert.throws(function () { router.use('/', 42) }, /argument handler must be a function/) }) it('should reject null as middleware', function () { var router = new Router() - assert.throws(function () { router.use('/', null) }, /requires a middleware function but got a Null/) + assert.throws(function () { router.use('/', null) }, /argument handler must be a function/) }) it('should reject Date as middleware', function () { var router = new Router() - assert.throws(function () { router.use('/', new Date()) }, /requires a middleware function but got a Date/) + assert.throws(function () { router.use('/', new Date()) }, /argument handler must be a function/) }) it('should be called for any URL', function (done) { var cb = after(4, done) var router = new Router() - function no () { + function no() { throw new Error('should not be called') } @@ -488,39 +474,49 @@ describe('Router', function(){ router.handle({ url: '*', method: 'GET' }, { end: cb }, no) }) - it('should accept array of middleware', function(done){ + it('should accept array of middleware', function (done) { var count = 0; var router = new Router(); - function fn1(req, res, next){ + function fn1(req, res, next) { assert.equal(++count, 1); next(); } - function fn2(req, res, next){ + function fn2(req, res, next) { assert.equal(++count, 2); next(); } - router.use([fn1, fn2], function(req, res){ + router.use([fn1, fn2], function (req, res) { assert.equal(++count, 3); done(); }); - router.handle({ url: '/foo', method: 'GET' }, {}, function(){}); + router.handle({ url: '/foo', method: 'GET' }, {}, function () { }); }) }) - describe('.param', function() { - it('should call param function when routing VERBS', function(done) { + describe('.param', function () { + it('should require function', function () { + var router = new Router(); + assert.throws(router.param.bind(router, 'id'), /argument fn is required/); + }); + + it('should reject non-function', function () { + var router = new Router(); + assert.throws(router.param.bind(router, 'id', 42), /argument fn must be a function/); + }); + + it('should call param function when routing VERBS', function (done) { var router = new Router(); - router.param('id', function(req, res, next, id) { + router.param('id', function (req, res, next, id) { assert.equal(id, '123'); next(); }); - router.get('/foo/:id/bar', function(req, res, next) { + router.get('/foo/:id/bar', function (req, res, next) { assert.equal(req.params.id, '123'); next(); }); @@ -528,15 +524,15 @@ describe('Router', function(){ router.handle({ url: '/foo/123/bar', method: 'get' }, {}, done); }); - it('should call param function when routing middleware', function(done) { + it('should call param function when routing middleware', function (done) { var router = new Router(); - router.param('id', function(req, res, next, id) { + router.param('id', function (req, res, next, id) { assert.equal(id, '123'); next(); }); - router.use('/foo/:id/bar', function(req, res, next) { + router.use('/foo/:id/bar', function (req, res, next) { assert.equal(req.params.id, '123'); assert.equal(req.url, '/baz'); next(); @@ -545,17 +541,17 @@ describe('Router', function(){ router.handle({ url: '/foo/123/bar/baz', method: 'get' }, {}, done); }); - it('should only call once per request', function(done) { + it('should only call once per request', function (done) { var count = 0; var req = { url: '/foo/bob/bar', method: 'get' }; var router = new Router(); var sub = new Router(); - sub.get('/bar', function(req, res, next) { + sub.get('/bar', function (req, res, next) { next(); }); - router.param('user', function(req, res, next, user) { + router.param('user', function (req, res, next, user) { count++; req.user = user; next(); @@ -564,7 +560,7 @@ describe('Router', function(){ router.use('/foo/:user/', new Router()); router.use('/foo/:user/', sub); - router.handle(req, {}, function(err) { + router.handle(req, {}, function (err) { if (err) return done(err); assert.equal(count, 1); assert.equal(req.user, 'bob'); @@ -572,17 +568,17 @@ describe('Router', function(){ }); }); - it('should call when values differ', function(done) { + it('should call when values differ', function (done) { var count = 0; var req = { url: '/foo/bob/bar', method: 'get' }; var router = new Router(); var sub = new Router(); - sub.get('/bar', function(req, res, next) { + sub.get('/bar', function (req, res, next) { next(); }); - router.param('user', function(req, res, next, user) { + router.param('user', function (req, res, next, user) { count++; req.user = user; next(); @@ -591,7 +587,7 @@ describe('Router', function(){ router.use('/foo/:user/', new Router()); router.use('/:user/bob/', sub); - router.handle(req, {}, function(err) { + router.handle(req, {}, function (err) { if (err) return done(err); assert.equal(count, 2); assert.equal(req.user, 'foo'); @@ -600,8 +596,8 @@ describe('Router', function(){ }); }); - describe('parallel requests', function() { - it('should not mix requests', function(done) { + describe('parallel requests', function () { + it('should not mix requests', function (done) { var req1 = { url: '/foo/50/bar', method: 'get' }; var req2 = { url: '/foo/10/bar', method: 'get' }; var router = new Router(); @@ -609,11 +605,11 @@ describe('Router', function(){ var cb = after(2, done) - sub.get('/bar', function(req, res, next) { + sub.get('/bar', function (req, res, next) { next(); }); - router.param('ms', function(req, res, next, ms) { + router.param('ms', function (req, res, next, ms) { ms = parseInt(ms, 10); req.ms = ms; setTimeout(next, ms); @@ -622,14 +618,14 @@ describe('Router', function(){ router.use('/foo/:ms/', new Router()); router.use('/foo/:ms/', sub); - router.handle(req1, {}, function(err) { + router.handle(req1, {}, function (err) { assert.ifError(err); assert.equal(req1.ms, 50); assert.equal(req1.originalUrl, '/foo/50/bar'); cb() }); - router.handle(req2, {}, function(err) { + router.handle(req2, {}, function (err) { assert.ifError(err); assert.equal(req2.ms, 10); assert.equal(req2.originalUrl, '/foo/10/bar'); diff --git a/test/app.all.js b/test/app.all.js index 185a8332fe7..e4afca7d731 100644 --- a/test/app.all.js +++ b/test/app.all.js @@ -26,7 +26,7 @@ describe('app.all()', function(){ var app = express() , n = 0; - app.all('/*', function(req, res, next){ + app.all('/*splat', function(req, res, next){ if (n++) return done(new Error('DELETE called several times')); next(); }); diff --git a/test/app.del.js b/test/app.del.js deleted file mode 100644 index e9e5769d658..00000000000 --- a/test/app.del.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' - -var express = require('../') - , request = require('supertest'); - -describe('app.del()', function(){ - it('should alias app.delete()', function(done){ - var app = express(); - - app.del('/tobi', function(req, res){ - res.end('deleted tobi!'); - }); - - request(app) - .del('/tobi') - .expect('deleted tobi!', done); - }) -}) diff --git a/test/app.engine.js b/test/app.engine.js index 214510a94c0..b0553aa247e 100644 --- a/test/app.engine.js +++ b/test/app.engine.js @@ -1,9 +1,9 @@ 'use strict' -var assert = require('assert') +var assert = require('node:assert') var express = require('../') - , fs = require('fs'); -var path = require('path') + , fs = require('node:fs'); +var path = require('node:path') function render(path, options, fn) { fs.readFile(path, 'utf8', function(err, str){ diff --git a/test/app.head.js b/test/app.head.js index fabb98795ab..0207caaedad 100644 --- a/test/app.head.js +++ b/test/app.head.js @@ -2,7 +2,7 @@ var express = require('../'); var request = require('supertest'); -var assert = require('assert'); +var assert = require('node:assert'); describe('HEAD', function(){ it('should default to GET', function(done){ diff --git a/test/app.js b/test/app.js index 6134717c33e..c1e815a052d 100644 --- a/test/app.js +++ b/test/app.js @@ -1,6 +1,6 @@ 'use strict' -var assert = require('assert') +var assert = require('node:assert') var express = require('..') var request = require('supertest') @@ -56,18 +56,6 @@ describe('app.mountpath', function(){ }) }) -describe('app.router', function(){ - it('should throw with notice', function(done){ - var app = express() - - try { - app.router; - } catch(err) { - done(); - } - }) -}) - describe('app.path()', function(){ it('should return the canonical', function(){ var app = express() diff --git a/test/app.listen.js b/test/app.listen.js index 5b150063b9e..180162a0fae 100644 --- a/test/app.listen.js +++ b/test/app.listen.js @@ -1,6 +1,7 @@ 'use strict' var express = require('../') +var assert = require('node:assert') describe('app.listen()', function(){ it('should wrap with an HTTP server', function(done){ @@ -10,4 +11,17 @@ describe('app.listen()', function(){ server.close(done) }); }) + it('should callback on HTTP server errors', function (done) { + var app1 = express() + var app2 = express() + + var server1 = app1.listen(0, function (err) { + assert(!err) + app2.listen(server1.address().port, function (err) { + assert(err.code === 'EADDRINUSE') + server1.close() + done() + }) + }) + }) }) diff --git a/test/app.locals.js b/test/app.locals.js index 657b4b75c79..3963762fe2b 100644 --- a/test/app.locals.js +++ b/test/app.locals.js @@ -1,14 +1,15 @@ 'use strict' -var assert = require('assert') +var assert = require('node:assert') var express = require('../') describe('app', function(){ describe('.locals', function () { - it('should default object', function () { + it('should default object with null prototype', function () { var app = express() assert.ok(app.locals) assert.strictEqual(typeof app.locals, 'object') + assert.strictEqual(Object.getPrototypeOf(app.locals), null) }) describe('.settings', function () { diff --git a/test/app.options.js b/test/app.options.js index fdfd38c8a28..ee4c81631cb 100644 --- a/test/app.options.js +++ b/test/app.options.js @@ -7,28 +7,28 @@ describe('OPTIONS', function(){ it('should default to the routes defined', function(done){ var app = express(); - app.del('/', function(){}); + app.post('/', function(){}); app.get('/users', function(req, res){}); app.put('/users', function(req, res){}); request(app) .options('/users') - .expect('Allow', 'GET,HEAD,PUT') - .expect(200, 'GET,HEAD,PUT', done); + .expect('Allow', 'GET, HEAD, PUT') + .expect(200, 'GET, HEAD, PUT', done); }) it('should only include each method once', function(done){ var app = express(); - app.del('/', function(){}); + app.delete('/', function(){}); app.get('/users', function(req, res){}); app.put('/users', function(req, res){}); app.get('/users', function(req, res){}); request(app) .options('/users') - .expect('Allow', 'GET,HEAD,PUT') - .expect(200, 'GET,HEAD,PUT', done); + .expect('Allow', 'GET, HEAD, PUT') + .expect(200, 'GET, HEAD, PUT', done); }) it('should not be affected by app.all', function(done){ @@ -45,8 +45,8 @@ describe('OPTIONS', function(){ request(app) .options('/users') .expect('x-hit', '1') - .expect('Allow', 'GET,HEAD,PUT') - .expect(200, 'GET,HEAD,PUT', done); + .expect('Allow', 'GET, HEAD, PUT') + .expect(200, 'GET, HEAD, PUT', done); }) it('should not respond if the path is not defined', function(done){ @@ -69,8 +69,8 @@ describe('OPTIONS', function(){ request(app) .options('/other') - .expect('Allow', 'GET,HEAD') - .expect(200, 'GET,HEAD', done); + .expect('Allow', 'GET, HEAD') + .expect(200, 'GET, HEAD', done); }) describe('when error occurs in response handler', function () { diff --git a/test/app.param.js b/test/app.param.js index b4ccc8a2d12..5c9a5630870 100644 --- a/test/app.param.js +++ b/test/app.param.js @@ -1,51 +1,9 @@ 'use strict' -var assert = require('assert') var express = require('../') , request = require('supertest'); describe('app', function(){ - describe('.param(fn)', function(){ - it('should map app.param(name, ...) logic', function(done){ - var app = express(); - - app.param(function(name, regexp){ - if (Object.prototype.toString.call(regexp) === '[object RegExp]') { // See #1557 - return function(req, res, next, val){ - var captures; - if (captures = regexp.exec(String(val))) { - req.params[name] = captures[1]; - next(); - } else { - next('route'); - } - } - } - }) - - app.param(':name', /^([a-zA-Z]+)$/); - - app.get('/user/:name', function(req, res){ - res.send(req.params.name); - }); - - request(app) - .get('/user/tj') - .expect(200, 'tj', function (err) { - if (err) return done(err) - request(app) - .get('/user/123') - .expect(404, done); - }); - - }) - - it('should fail if not given fn', function(){ - var app = express(); - assert.throws(app.param.bind(app, ':name', 'bob')) - }) - }) - describe('.param(names, fn)', function(){ it('should map the array', function(done){ var app = express(); diff --git a/test/app.render.js b/test/app.render.js index 9d202acfdda..ca15e761d35 100644 --- a/test/app.render.js +++ b/test/app.render.js @@ -1,8 +1,8 @@ 'use strict' -var assert = require('assert') +var assert = require('node:assert') var express = require('..'); -var path = require('path') +var path = require('node:path') var tmpl = require('./support/tmpl'); describe('app', function(){ diff --git a/test/app.request.js b/test/app.request.js index 4930af84c25..b6c00f5baa3 100644 --- a/test/app.request.js +++ b/test/app.request.js @@ -10,7 +10,7 @@ describe('app', function(){ var app = express(); app.request.querystring = function(){ - return require('url').parse(this.url).query; + return require('node:url').parse(this.url).query; }; app.use(function(req, res){ diff --git a/test/app.route.js b/test/app.route.js index eaf8a120515..03ae1293685 100644 --- a/test/app.route.js +++ b/test/app.route.js @@ -61,4 +61,137 @@ describe('app.route', function(){ .get('/test') .expect(404, done); }); + + describe('promise support', function () { + it('should pass rejected promise value', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function helloWorld (req, res) { + res.send('hello, world!') + }) + + route.all(function handleError (err, req, res, next) { + res.status(500) + res.send('caught: ' + err.message) + }) + + request(app) + .get('/foo') + .expect(500, 'caught: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + return Promise.reject() + }) + + route.all(function helloWorld (req, res) { + res.send('hello, world!') + }) + + route.all(function handleError (err, req, res, next) { + res.status(500) + res.send('caught: ' + err.message) + }) + + request(app) + .get('/foo') + .expect(500, 'caught: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + res.send('saw GET /foo') + return Promise.resolve('foo') + }) + + route.all(function () { + done(new Error('Unexpected route invoke')) + }) + + request(app) + .get('/foo') + .expect(200, 'saw GET /foo', done) + }) + + describe('error handling', function () { + it('should pass rejected promise value', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + route.all(function handleError (err, req, res, next) { + res.status(500) + res.send('caught again: ' + err.message) + }) + + request(app) + .get('/foo') + .expect(500, 'caught again: caught: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + return Promise.reject() + }) + + route.all(function handleError (err, req, res, next) { + res.status(500) + res.send('caught again: ' + err.message) + }) + + request(app) + .get('/foo') + .expect(500, 'caught again: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var app = express() + var route = app.route('/foo') + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + res.status(500) + res.send('caught: ' + err.message) + return Promise.resolve('foo') + }) + + route.all(function () { + done(new Error('Unexpected route invoke')) + }) + + request(app) + .get('/foo') + .expect(500, 'caught: boom!', done) + }) + }) + }) }); diff --git a/test/app.router.js b/test/app.router.js index 12b6c1fa519..017f4f4ef45 100644 --- a/test/app.router.js +++ b/test/app.router.js @@ -3,24 +3,26 @@ var after = require('after'); var express = require('../') , request = require('supertest') - , assert = require('assert') - , methods = require('methods'); + , assert = require('node:assert') + , methods = require('../lib/utils').methods; -describe('app.router', function(){ - it('should restore req.params after leaving router', function(done){ +var shouldSkipQuery = require('./support/utils').shouldSkipQuery + +describe('app.router', function () { + it('should restore req.params after leaving router', function (done) { var app = express(); var router = new express.Router(); - function handler1(req, res, next){ + function handler1(req, res, next) { res.setHeader('x-user-id', String(req.params.id)); next() } - function handler2(req, res){ + function handler2(req, res) { res.send(req.params.id); } - router.use(function(req, res, next){ + router.use(function (req, res, next) { res.setHeader('x-router', String(req.params.id)); next(); }); @@ -28,31 +30,34 @@ describe('app.router', function(){ app.get('/user/:id', handler1, router, handler2); request(app) - .get('/user/1') - .expect('x-router', 'undefined') - .expect('x-user-id', '1') - .expect(200, '1', done); + .get('/user/1') + .expect('x-router', 'undefined') + .expect('x-user-id', '1') + .expect(200, '1', done); }) - describe('methods', function(){ - methods.concat('del').forEach(function(method){ + describe('methods', function () { + methods.forEach(function (method) { if (method === 'connect') return; - it('should include ' + method.toUpperCase(), function(done){ + it('should include ' + method.toUpperCase(), function (done) { + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } var app = express(); - app[method]('/foo', function(req, res){ + app[method]('/foo', function (req, res) { res.send(method) }); request(app) [method]('/foo') - .expect(200, done) + .expect(200, done) }) - it('should reject numbers for app.' + method, function(){ + it('should reject numbers for app.' + method, function () { var app = express(); - assert.throws(app[method].bind(app, '/', 3), /Number/) + assert.throws(app[method].bind(app, '/', 3), /argument handler must be a function/); }) }); @@ -72,22 +77,22 @@ describe('app.router', function(){ }); request(app) - .get('/') - .expect(404, cb) + .get('/') + .expect(404, cb) request(app) - .delete('/') - .expect(200, 'deleted everything', cb); + .delete('/') + .expect(200, 'deleted everything', cb); request(app) - .post('/') - .expect('X-Method-Altered', '1') - .expect(200, 'deleted everything', cb); + .post('/') + .expect('X-Method-Altered', '1') + .expect(200, 'deleted everything', cb); }); }) describe('decode params', function () { - it('should decode correct params', function(done){ + it('should decode correct params', function (done) { var app = express(); app.get('/:name', function (req, res) { @@ -95,11 +100,11 @@ describe('app.router', function(){ }); request(app) - .get('/foo%2Fbar') - .expect('foo/bar', done); + .get('/foo%2Fbar') + .expect('foo/bar', done); }) - it('should not accept params in malformed paths', function(done) { + it('should not accept params in malformed paths', function (done) { var app = express(); app.get('/:name', function (req, res) { @@ -107,11 +112,11 @@ describe('app.router', function(){ }); request(app) - .get('/%foobar') - .expect(400, done); + .get('/%foobar') + .expect(400, done); }) - it('should not decode spaces', function(done) { + it('should not decode spaces', function (done) { var app = express(); app.get('/:name', function (req, res) { @@ -119,11 +124,11 @@ describe('app.router', function(){ }); request(app) - .get('/foo+bar') - .expect('foo+bar', done); + .get('/foo+bar') + .expect('foo+bar', done); }) - it('should work with unicode', function(done) { + it('should work with unicode', function (done) { var app = express(); app.get('/:name', function (req, res) { @@ -131,63 +136,80 @@ describe('app.router', function(){ }); request(app) - .get('/%ce%b1') - .expect('\u03b1', done); + .get('/%ce%b1') + .expect('\u03b1', done); }) }) - it('should be .use()able', function(done){ + it('should be .use()able', function (done) { var app = express(); var calls = []; - app.use(function(req, res, next){ + app.use(function (req, res, next) { calls.push('before'); next(); }); - app.get('/', function(req, res, next){ + app.get('/', function (req, res, next) { calls.push('GET /') next(); }); - app.use(function(req, res, next){ + app.use(function (req, res, next) { calls.push('after'); res.json(calls) }); request(app) - .get('/') - .expect(200, ['before', 'GET /', 'after'], done) + .get('/') + .expect(200, ['before', 'GET /', 'after'], done) }) - describe('when given a regexp', function(){ - it('should match the pathname only', function(done){ + describe('when given a regexp', function () { + it('should match the pathname only', function (done) { var app = express(); - app.get(/^\/user\/[0-9]+$/, function(req, res){ + app.get(/^\/user\/[0-9]+$/, function (req, res) { res.end('user'); }); request(app) - .get('/user/12?foo=bar') - .expect('user', done); + .get('/user/12?foo=bar') + .expect('user', done); }) - it('should populate req.params with the captures', function(done){ + it('should populate req.params with the captures', function (done) { var app = express(); - app.get(/^\/user\/([0-9]+)\/(view|edit)?$/, function(req, res){ + app.get(/^\/user\/([0-9]+)\/(view|edit)?$/, function (req, res) { var id = req.params[0] , op = req.params[1]; res.end(op + 'ing user ' + id); }); request(app) - .get('/user/10/edit') - .expect('editing user 10', done); + .get('/user/10/edit') + .expect('editing user 10', done); }) + if (supportsRegexp('(?.*)')) { + it('should populate req.params with named captures', function (done) { + var app = express(); + var re = new RegExp('^/user/(?[0-9]+)/(view|edit)?$'); + + app.get(re, function (req, res) { + var id = req.params.userId + , op = req.params[0]; + res.end(op + 'ing user ' + id); + }); + + request(app) + .get('/user/10/edit') + .expect('editing user 10', done); + }) + } + it('should ensure regexp matches path prefix', function (done) { var app = express() var p = [] @@ -218,153 +240,153 @@ describe('app.router', function(){ }) }) - describe('case sensitivity', function(){ - it('should be disabled by default', function(done){ + describe('case sensitivity', function () { + it('should be disabled by default', function (done) { var app = express(); - app.get('/user', function(req, res){ + app.get('/user', function (req, res) { res.end('tj'); }); request(app) - .get('/USER') - .expect('tj', done); + .get('/USER') + .expect('tj', done); }) - describe('when "case sensitive routing" is enabled', function(){ - it('should match identical casing', function(done){ + describe('when "case sensitive routing" is enabled', function () { + it('should match identical casing', function (done) { var app = express(); app.enable('case sensitive routing'); - app.get('/uSer', function(req, res){ + app.get('/uSer', function (req, res) { res.end('tj'); }); request(app) - .get('/uSer') - .expect('tj', done); + .get('/uSer') + .expect('tj', done); }) - it('should not match otherwise', function(done){ + it('should not match otherwise', function (done) { var app = express(); app.enable('case sensitive routing'); - app.get('/uSer', function(req, res){ + app.get('/uSer', function (req, res) { res.end('tj'); }); request(app) - .get('/user') - .expect(404, done); + .get('/user') + .expect(404, done); }) }) }) - describe('params', function(){ - it('should overwrite existing req.params by default', function(done){ + describe('params', function () { + it('should overwrite existing req.params by default', function (done) { var app = express(); var router = new express.Router(); - router.get('/:action', function(req, res){ + router.get('/:action', function (req, res) { res.send(req.params); }); app.use('/user/:user', router); request(app) - .get('/user/1/get') - .expect(200, '{"action":"get"}', done); + .get('/user/1/get') + .expect(200, '{"action":"get"}', done); }) - it('should allow merging existing req.params', function(done){ + it('should allow merging existing req.params', function (done) { var app = express(); var router = new express.Router({ mergeParams: true }); - router.get('/:action', function(req, res){ + router.get('/:action', function (req, res) { var keys = Object.keys(req.params).sort(); - res.send(keys.map(function(k){ return [k, req.params[k]] })); + res.send(keys.map(function (k) { return [k, req.params[k]] })); }); app.use('/user/:user', router); request(app) - .get('/user/tj/get') - .expect(200, '[["action","get"],["user","tj"]]', done); + .get('/user/tj/get') + .expect(200, '[["action","get"],["user","tj"]]', done); }) - it('should use params from router', function(done){ + it('should use params from router', function (done) { var app = express(); var router = new express.Router({ mergeParams: true }); - router.get('/:thing', function(req, res){ + router.get('/:thing', function (req, res) { var keys = Object.keys(req.params).sort(); - res.send(keys.map(function(k){ return [k, req.params[k]] })); + res.send(keys.map(function (k) { return [k, req.params[k]] })); }); app.use('/user/:thing', router); request(app) - .get('/user/tj/get') - .expect(200, '[["thing","get"]]', done); + .get('/user/tj/get') + .expect(200, '[["thing","get"]]', done); }) - it('should merge numeric indices req.params', function(done){ + it('should merge numeric indices req.params', function (done) { var app = express(); var router = new express.Router({ mergeParams: true }); - router.get('/*.*', function(req, res){ + router.get(/^\/(.*)\.(.*)/, function (req, res) { var keys = Object.keys(req.params).sort(); - res.send(keys.map(function(k){ return [k, req.params[k]] })); + res.send(keys.map(function (k) { return [k, req.params[k]] })); }); - app.use('/user/id:(\\d+)', router); + app.use(/^\/user\/id:(\d+)/, router); request(app) - .get('/user/id:10/profile.json') - .expect(200, '[["0","10"],["1","profile"],["2","json"]]', done); + .get('/user/id:10/profile.json') + .expect(200, '[["0","10"],["1","profile"],["2","json"]]', done); }) - it('should merge numeric indices req.params when more in parent', function(done){ + it('should merge numeric indices req.params when more in parent', function (done) { var app = express(); var router = new express.Router({ mergeParams: true }); - router.get('/*', function(req, res){ + router.get(/\/(.*)/, function (req, res) { var keys = Object.keys(req.params).sort(); - res.send(keys.map(function(k){ return [k, req.params[k]] })); + res.send(keys.map(function (k) { return [k, req.params[k]] })); }); - app.use('/user/id:(\\d+)/name:(\\w+)', router); + app.use(/^\/user\/id:(\d+)\/name:(\w+)/, router); request(app) - .get('/user/id:10/name:tj/profile') - .expect(200, '[["0","10"],["1","tj"],["2","profile"]]', done); + .get('/user/id:10/name:tj/profile') + .expect(200, '[["0","10"],["1","tj"],["2","profile"]]', done); }) - it('should merge numeric indices req.params when parent has same number', function(done){ + it('should merge numeric indices req.params when parent has same number', function (done) { var app = express(); var router = new express.Router({ mergeParams: true }); - router.get('/name:(\\w+)', function(req, res){ + router.get(/\/name:(\w+)/, function (req, res) { var keys = Object.keys(req.params).sort(); - res.send(keys.map(function(k){ return [k, req.params[k]] })); + res.send(keys.map(function (k) { return [k, req.params[k]] })); }); - app.use('/user/id:(\\d+)', router); + app.use(/\/user\/id:(\d+)/, router); request(app) - .get('/user/id:10/name:tj') - .expect(200, '[["0","10"],["1","tj"]]', done); + .get('/user/id:10/name:tj') + .expect(200, '[["0","10"],["1","tj"]]', done); }) - it('should ignore invalid incoming req.params', function(done){ + it('should ignore invalid incoming req.params', function (done) { var app = express(); var router = new express.Router({ mergeParams: true }); - router.get('/:name', function(req, res){ + router.get('/:name', function (req, res) { var keys = Object.keys(req.params).sort(); - res.send(keys.map(function(k){ return [k, req.params[k]] })); + res.send(keys.map(function (k) { return [k, req.params[k]] })); }); app.use('/user/', function (req, res, next) { @@ -373,60 +395,60 @@ describe('app.router', function(){ }); request(app) - .get('/user/tj') - .expect(200, '[["name","tj"]]', done); + .get('/user/tj') + .expect(200, '[["name","tj"]]', done); }) - it('should restore req.params', function(done){ + it('should restore req.params', function (done) { var app = express(); var router = new express.Router({ mergeParams: true }); - router.get('/user:(\\w+)/*', function (req, res, next) { + router.get(/\/user:(\w+)\//, function (req, res, next) { next(); }); - app.use('/user/id:(\\d+)', function (req, res, next) { + app.use(/\/user\/id:(\d+)/, function (req, res, next) { router(req, res, function (err) { var keys = Object.keys(req.params).sort(); - res.send(keys.map(function(k){ return [k, req.params[k]] })); + res.send(keys.map(function (k) { return [k, req.params[k]] })); }); }); request(app) - .get('/user/id:42/user:tj/profile') - .expect(200, '[["0","42"]]', done); + .get('/user/id:42/user:tj/profile') + .expect(200, '[["0","42"]]', done); }) }) - describe('trailing slashes', function(){ - it('should be optional by default', function(done){ + describe('trailing slashes', function () { + it('should be optional by default', function (done) { var app = express(); - app.get('/user', function(req, res){ + app.get('/user', function (req, res) { res.end('tj'); }); request(app) - .get('/user/') - .expect('tj', done); + .get('/user/') + .expect('tj', done); }) - describe('when "strict routing" is enabled', function(){ - it('should match trailing slashes', function(done){ + describe('when "strict routing" is enabled', function () { + it('should match trailing slashes', function (done) { var app = express(); app.enable('strict routing'); - app.get('/user/', function(req, res){ + app.get('/user/', function (req, res) { res.end('tj'); }); request(app) - .get('/user/') - .expect('tj', done); + .get('/user/') + .expect('tj', done); }) - it('should pass-though middleware', function(done){ + it('should pass-though middleware', function (done) { var app = express(); app.enable('strict routing'); @@ -436,17 +458,17 @@ describe('app.router', function(){ next(); }); - app.get('/user/', function(req, res){ + app.get('/user/', function (req, res) { res.end('tj'); }); request(app) - .get('/user/') - .expect('x-middleware', 'true') - .expect(200, 'tj', done); + .get('/user/') + .expect('x-middleware', 'true') + .expect(200, 'tj', done); }) - it('should pass-though mounted middleware', function(done){ + it('should pass-though mounted middleware', function (done) { var app = express(); app.enable('strict routing'); @@ -456,123 +478,106 @@ describe('app.router', function(){ next(); }); - app.get('/user/test/', function(req, res){ + app.get('/user/test/', function (req, res) { res.end('tj'); }); request(app) - .get('/user/test/') - .expect('x-middleware', 'true') - .expect(200, 'tj', done); + .get('/user/test/') + .expect('x-middleware', 'true') + .expect(200, 'tj', done); }) - it('should match no slashes', function(done){ + it('should match no slashes', function (done) { var app = express(); app.enable('strict routing'); - app.get('/user', function(req, res){ + app.get('/user', function (req, res) { res.end('tj'); }); request(app) - .get('/user') - .expect('tj', done); + .get('/user') + .expect('tj', done); }) - it('should match middleware when omitting the trailing slash', function(done){ + it('should match middleware when omitting the trailing slash', function (done) { var app = express(); app.enable('strict routing'); - app.use('/user/', function(req, res){ + app.use('/user/', function (req, res) { res.end('tj'); }); request(app) - .get('/user') - .expect(200, 'tj', done); + .get('/user') + .expect(200, 'tj', done); }) - it('should match middleware', function(done){ + it('should match middleware', function (done) { var app = express(); app.enable('strict routing'); - app.use('/user', function(req, res){ + app.use('/user', function (req, res) { res.end('tj'); }); request(app) - .get('/user') - .expect(200, 'tj', done); + .get('/user') + .expect(200, 'tj', done); }) - it('should match middleware when adding the trailing slash', function(done){ + it('should match middleware when adding the trailing slash', function (done) { var app = express(); app.enable('strict routing'); - app.use('/user', function(req, res){ + app.use('/user', function (req, res) { res.end('tj'); }); request(app) - .get('/user/') - .expect(200, 'tj', done); + .get('/user/') + .expect(200, 'tj', done); }) - it('should fail when omitting the trailing slash', function(done){ + it('should fail when omitting the trailing slash', function (done) { var app = express(); app.enable('strict routing'); - app.get('/user/', function(req, res){ + app.get('/user/', function (req, res) { res.end('tj'); }); request(app) - .get('/user') - .expect(404, done); + .get('/user') + .expect(404, done); }) - it('should fail when adding the trailing slash', function(done){ + it('should fail when adding the trailing slash', function (done) { var app = express(); app.enable('strict routing'); - app.get('/user', function(req, res){ + app.get('/user', function (req, res) { res.end('tj'); }); request(app) - .get('/user/') - .expect(404, done); + .get('/user/') + .expect(404, done); }) }) }) - it('should allow escaped regexp', function(done){ - var app = express(); - - app.get('/user/\\d+', function(req, res){ - res.end('woot'); - }); - - request(app) - .get('/user/10') - .expect(200, function (err) { - if (err) return done(err) - request(app) - .get('/user/tj') - .expect(404, done); - }); - }) - - it('should allow literal "."', function(done){ + it('should allow literal "."', function (done) { var app = express(); - app.get('/api/users/:from..:to', function(req, res){ + app.get('/api/users/:from..:to', function (req, res) { var from = req.params.from , to = req.params.to; @@ -580,294 +585,204 @@ describe('app.router', function(){ }); request(app) - .get('/api/users/1..50') - .expect('users from 1 to 50', done); + .get('/api/users/1..50') + .expect('users from 1 to 50', done); }) - describe('*', function(){ - it('should capture everything', function (done) { - var app = express() - - app.get('*', function (req, res) { - res.end(req.params[0]) - }) - - request(app) - .get('/user/tobi.json') - .expect('/user/tobi.json', done) - }) - - it('should decode the capture', function (done) { - var app = express() - - app.get('*', function (req, res) { - res.end(req.params[0]) - }) - - request(app) - .get('/user/tobi%20and%20loki.json') - .expect('/user/tobi and loki.json', done) - }) - - it('should denote a greedy capture group', function(done){ + describe(':name', function () { + it('should denote a capture group', function (done) { var app = express(); - app.get('/user/*.json', function(req, res){ - res.end(req.params[0]); - }); - - request(app) - .get('/user/tj.json') - .expect('tj', done); - }) - - it('should work with several', function(done){ - var app = express(); - - app.get('/api/*.*', function(req, res){ - var resource = req.params[0] - , format = req.params[1]; - res.end(resource + ' as ' + format); - }); - - request(app) - .get('/api/users/foo.bar.json') - .expect('users/foo.bar as json', done); - }) - - it('should work cross-segment', function(done){ - var app = express(); - var cb = after(2, done) - - app.get('/api*', function(req, res){ - res.send(req.params[0]); - }); - - request(app) - .get('/api') - .expect(200, '', cb) - - request(app) - .get('/api/hey') - .expect(200, '/hey', cb) - }) - - it('should allow naming', function(done){ - var app = express(); - - app.get('/api/:resource(*)', function(req, res){ - var resource = req.params.resource; - res.end(resource); + app.get('/user/:user', function (req, res) { + res.end(req.params.user); }); request(app) - .get('/api/users/0.json') - .expect('users/0.json', done); + .get('/user/tj') + .expect('tj', done); }) - it('should not be greedy immediately after param', function(done){ + it('should match a single segment only', function (done) { var app = express(); - app.get('/user/:user*', function(req, res){ + app.get('/user/:user', function (req, res) { res.end(req.params.user); }); request(app) - .get('/user/122') - .expect('122', done); + .get('/user/tj/edit') + .expect(404, done); }) - it('should eat everything after /', function(done){ + it('should allow several capture groups', function (done) { var app = express(); - app.get('/user/:user*', function(req, res){ - res.end(req.params.user); + app.get('/user/:user/:op', function (req, res) { + res.end(req.params.op + 'ing ' + req.params.user); }); request(app) - .get('/user/122/aaa') - .expect('122', done); + .get('/user/tj/edit') + .expect('editing tj', done); }) - it('should span multiple segments', function(done){ + it('should work following a partial capture group', function (done) { var app = express(); + var cb = after(2, done); - app.get('/file/*', function(req, res){ - res.end(req.params[0]); + app.get('/user{s}/:user/:op', function (req, res) { + res.end(req.params.op + 'ing ' + req.params.user + (req.url.startsWith('/users') ? ' (old)' : '')); }); request(app) - .get('/file/javascripts/jquery.js') - .expect('javascripts/jquery.js', done); - }) - - it('should be optional', function(done){ - var app = express(); - - app.get('/file/*', function(req, res){ - res.end(req.params[0]); - }); + .get('/user/tj/edit') + .expect('editing tj', cb); request(app) - .get('/file/') - .expect('', done); + .get('/users/tj/edit') + .expect('editing tj (old)', cb); }) - it('should require a preceding /', function(done){ + it('should work inside literal parenthesis', function (done) { var app = express(); - app.get('/file/*', function(req, res){ - res.end(req.params[0]); + app.get('/:user\\(:op\\)', function (req, res) { + res.end(req.params.op + 'ing ' + req.params.user); }); request(app) - .get('/file') - .expect(404, done); + .get('/tj(edit)') + .expect('editing tj', done); }) - it('should keep correct parameter indexes', function(done){ + it('should work in array of paths', function (done) { var app = express(); + var cb = after(2, done); - app.get('/*/user/:id', function (req, res) { - res.send(req.params); + app.get(['/user/:user/poke', '/user/:user/pokes'], function (req, res) { + res.end('poking ' + req.params.user); }); request(app) - .get('/1/user/2') - .expect(200, '{"0":"1","id":"2"}', done); - }) - - it('should work within arrays', function(done){ - var app = express(); - - app.get(['/user/:id', '/foo/*', '/:bar'], function (req, res) { - res.send(req.params.bar); - }); + .get('/user/tj/poke') + .expect('poking tj', cb); request(app) - .get('/test') - .expect(200, 'test', done); + .get('/user/tj/pokes') + .expect('poking tj', cb); }) }) - describe(':name', function(){ - it('should denote a capture group', function(done){ + describe(':name?', function () { + it('should denote an optional capture group', function (done) { var app = express(); - app.get('/user/:user', function(req, res){ - res.end(req.params.user); + app.get('/user/:user{/:op}', function (req, res) { + var op = req.params.op || 'view'; + res.end(op + 'ing ' + req.params.user); }); request(app) - .get('/user/tj') - .expect('tj', done); + .get('/user/tj') + .expect('viewing tj', done); }) - it('should match a single segment only', function(done){ + it('should populate the capture group', function (done) { var app = express(); - app.get('/user/:user', function(req, res){ - res.end(req.params.user); + app.get('/user/:user{/:op}', function (req, res) { + var op = req.params.op || 'view'; + res.end(op + 'ing ' + req.params.user); }); request(app) - .get('/user/tj/edit') - .expect(404, done); + .get('/user/tj/edit') + .expect('editing tj', done); }) + }) - it('should allow several capture groups', function(done){ - var app = express(); + describe(':name*', function () { + it('should match one segment', function (done) { + var app = express() - app.get('/user/:user/:op', function(req, res){ - res.end(req.params.op + 'ing ' + req.params.user); - }); + app.get('/user/*user', function (req, res) { + res.end(req.params.user[0]) + }) request(app) - .get('/user/tj/edit') - .expect('editing tj', done); + .get('/user/122') + .expect('122', done) }) - it('should work following a partial capture group', function(done){ - var app = express(); - var cb = after(2, done); - - app.get('/user(s)?/:user/:op', function(req, res){ - res.end(req.params.op + 'ing ' + req.params.user + (req.params[0] ? ' (old)' : '')); - }); + it('should match many segments', function (done) { + var app = express() - request(app) - .get('/user/tj/edit') - .expect('editing tj', cb); + app.get('/user/*user', function (req, res) { + res.end(req.params.user.join('/')) + }) request(app) - .get('/users/tj/edit') - .expect('editing tj (old)', cb); + .get('/user/1/2/3/4') + .expect('1/2/3/4', done) }) - it('should work inside literal parenthesis', function(done){ - var app = express(); + it('should match zero segments', function (done) { + var app = express() - app.get('/:user\\(:op\\)', function(req, res){ - res.end(req.params.op + 'ing ' + req.params.user); - }); + app.get('/user{/*user}', function (req, res) { + res.end(req.params.user) + }) request(app) - .get('/tj(edit)') - .expect('editing tj', done); + .get('/user') + .expect('', done) }) + }) - it('should work in array of paths', function(done){ - var app = express(); - var cb = after(2, done); - - app.get(['/user/:user/poke', '/user/:user/pokes'], function(req, res){ - res.end('poking ' + req.params.user); - }); + describe(':name+', function () { + it('should match one segment', function (done) { + var app = express() - request(app) - .get('/user/tj/poke') - .expect('poking tj', cb); + app.get('/user/*user', function (req, res) { + res.end(req.params.user[0]) + }) request(app) - .get('/user/tj/pokes') - .expect('poking tj', cb); + .get('/user/122') + .expect(200, '122', done) }) - }) - describe(':name?', function(){ - it('should denote an optional capture group', function(done){ - var app = express(); + it('should match many segments', function (done) { + var app = express() - app.get('/user/:user/:op?', function(req, res){ - var op = req.params.op || 'view'; - res.end(op + 'ing ' + req.params.user); - }); + app.get('/user/*user', function (req, res) { + res.end(req.params.user.join('/')) + }) request(app) - .get('/user/tj') - .expect('viewing tj', done); + .get('/user/1/2/3/4') + .expect(200, '1/2/3/4', done) }) - it('should populate the capture group', function(done){ - var app = express(); + it('should not match zero segments', function (done) { + var app = express() - app.get('/user/:user/:op?', function(req, res){ - var op = req.params.op || 'view'; - res.end(op + 'ing ' + req.params.user); - }); + app.get('/user/*user', function (req, res) { + res.end(req.params.user) + }) request(app) - .get('/user/tj/edit') - .expect('editing tj', done); + .get('/user') + .expect(404, done) }) }) - describe('.:name', function(){ - it('should denote a format', function(done){ + describe('.:name', function () { + it('should denote a format', function (done) { var app = express(); var cb = after(2, done) - app.get('/:name.:format', function(req, res){ + app.get('/:name.:format', function (req, res) { res.end(req.params.name + ' as ' + req.params.format); }); @@ -881,12 +796,12 @@ describe('app.router', function(){ }) }) - describe('.:name?', function(){ - it('should denote an optional format', function(done){ + describe('.:name?', function () { + it('should denote an optional format', function (done) { var app = express(); var cb = after(2, done) - app.get('/:name.:format?', function(req, res){ + app.get('/:name{.:format}', function (req, res) { res.end(req.params.name + ' as ' + (req.params.format || 'html')); }); @@ -900,12 +815,12 @@ describe('app.router', function(){ }) }) - describe('when next() is called', function(){ - it('should continue lookup', function(done){ + describe('when next() is called', function () { + it('should continue lookup', function (done) { var app = express() , calls = []; - app.get('/foo/:bar?', function(req, res, next){ + app.get('/foo{/:bar}', function (req, res, next) { calls.push('/foo/:bar?'); next(); }); @@ -914,7 +829,7 @@ describe('app.router', function(){ assert(0); }); - app.get('/foo', function(req, res, next){ + app.get('/foo', function (req, res, next) { calls.push('/foo'); next(); }); @@ -925,16 +840,16 @@ describe('app.router', function(){ }); request(app) - .get('/foo') - .expect(200, ['/foo/:bar?', '/foo', '/foo 2'], done) + .get('/foo') + .expect(200, ['/foo/:bar?', '/foo', '/foo 2'], done) }) }) - describe('when next("route") is called', function(){ - it('should jump to next route', function(done){ + describe('when next("route") is called', function () { + it('should jump to next route', function (done) { var app = express() - function fn(req, res, next){ + function fn(req, res, next) { res.set('X-Hit', '1') next('route') } @@ -943,14 +858,14 @@ describe('app.router', function(){ res.end('failure') }); - app.get('/foo', function(req, res){ + app.get('/foo', function (req, res) { res.end('success') }) request(app) - .get('/foo') - .expect('X-Hit', '1') - .expect(200, 'success', done) + .get('/foo') + .expect('X-Hit', '1') + .expect(200, 'success', done) }) }) @@ -959,7 +874,7 @@ describe('app.router', function(){ var app = express() var router = express.Router() - function fn (req, res, next) { + function fn(req, res, next) { res.set('X-Hit', '1') next('router') } @@ -979,18 +894,18 @@ describe('app.router', function(){ }) request(app) - .get('/foo') - .expect('X-Hit', '1') - .expect(200, 'success', done) + .get('/foo') + .expect('X-Hit', '1') + .expect(200, 'success', done) }) }) - describe('when next(err) is called', function(){ - it('should break out of app.router', function(done){ + describe('when next(err) is called', function () { + it('should break out of app.router', function (done) { var app = express() , calls = []; - app.get('/foo/:bar?', function(req, res, next){ + app.get('/foo{/:bar}', function (req, res, next) { calls.push('/foo/:bar?'); next(); }); @@ -999,7 +914,7 @@ describe('app.router', function(){ assert(0); }); - app.get('/foo', function(req, res, next){ + app.get('/foo', function (req, res, next) { calls.push('/foo'); next(new Error('fail')); }); @@ -1008,7 +923,7 @@ describe('app.router', function(){ assert(0); }); - app.use(function(err, req, res, next){ + app.use(function (err, req, res, next) { res.json({ calls: calls, error: err.message @@ -1016,11 +931,11 @@ describe('app.router', function(){ }) request(app) - .get('/foo') - .expect(200, { calls: ['/foo/:bar?', '/foo'], error: 'fail' }, done) + .get('/foo') + .expect(200, { calls: ['/foo/:bar?', '/foo'], error: 'fail' }, done) }) - it('should call handler in same route, if exists', function(done){ + it('should call handler in same route, if exists', function (done) { var app = express(); function fn1(req, res, next) { @@ -1042,70 +957,261 @@ describe('app.router', function(){ }) request(app) - .get('/foo') - .expect('route go boom!', done) + .get('/foo') + .expect('route go boom!', done) }) }) - it('should allow rewriting of the url', function(done){ + describe('promise support', function () { + it('should pass rejected promise value', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(function sawError(err, req, res, next) { + res.send('saw ' + err.name + ': ' + err.message) + }) + + app.use(router) + + request(app) + .get('/') + .expect(200, 'saw Error: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + return Promise.reject() + }) + + router.use(function sawError(err, req, res, next) { + res.send('saw ' + err.name + ': ' + err.message) + }) + + app.use(router) + + request(app) + .get('/') + .expect(200, 'saw Error: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + res.send('saw GET /foo') + return Promise.resolve('foo') + }) + + router.use(function () { + done(new Error('Unexpected middleware invoke')) + }) + + app.use(router) + + request(app) + .get('/foo') + .expect(200, 'saw GET /foo', done) + }) + + describe('error handling', function () { + it('should pass rejected promise value', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(function handleError(err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + router.use(function sawError(err, req, res, next) { + res.send('saw ' + err.name + ': ' + err.message) + }) + + app.use(router) + + request(app) + .get('/') + .expect(200, 'saw Error: caught: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + return Promise.reject() + }) + + router.use(function handleError(err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + router.use(function sawError(err, req, res, next) { + res.send('saw ' + err.name + ': ' + err.message) + }) + + app.use(router) + + request(app) + .get('/') + .expect(200, 'saw Error: caught: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var app = express() + var router = new express.Router() + + router.use(function createError(req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(function handleError(err, req, res, next) { + res.send('saw ' + err.name + ': ' + err.message) + return Promise.resolve('foo') + }) + + router.use(function () { + done(new Error('Unexpected middleware invoke')) + }) + + app.use(router) + + request(app) + .get('/foo') + .expect(200, 'saw Error: boom!', done) + }) + }) + }) + + it('should allow rewriting of the url', function (done) { var app = express(); - app.get('/account/edit', function(req, res, next){ + app.get('/account/edit', function (req, res, next) { req.user = { id: 12 }; // faux authenticated user req.url = '/user/' + req.user.id + '/edit'; next(); }); - app.get('/user/:id/edit', function(req, res){ + app.get('/user/:id/edit', function (req, res) { res.send('editing user ' + req.params.id); }); request(app) - .get('/account/edit') - .expect('editing user 12', done); + .get('/account/edit') + .expect('editing user 12', done); }) - it('should run in order added', function(done){ + it('should run in order added', function (done) { var app = express(); var path = []; - app.get('*', function(req, res, next){ + app.get('/*path', function (req, res, next) { path.push(0); next(); }); - app.get('/user/:id', function(req, res, next){ + app.get('/user/:id', function (req, res, next) { path.push(1); next(); }); - app.use(function(req, res, next){ + app.use(function (req, res, next) { path.push(2); next(); }); - app.all('/user/:id', function(req, res, next){ + app.all('/user/:id', function (req, res, next) { path.push(3); next(); }); - app.get('*', function(req, res, next){ + app.get('/*splat', function (req, res, next) { path.push(4); next(); }); - app.use(function(req, res, next){ + app.use(function (req, res, next) { path.push(5); res.end(path.join(',')) }); request(app) - .get('/user/1') - .expect(200, '0,1,2,3,4,5', done); + .get('/user/1') + .expect(200, '0,1,2,3,4,5', done); }) - it('should be chainable', function(){ + it('should be chainable', function () { var app = express(); - assert.strictEqual(app.get('/', function () {}), app) + assert.strictEqual(app.get('/', function () { }), app) + }) + + it('should should not use disposed router/middleware', function (done) { + // more context: https://github.com/expressjs/express/issues/5743#issuecomment-2277148412 + + var app = express(); + var router = new express.Router(); + + router.use(function (req, res, next) { + res.setHeader('old', 'foo'); + next(); + }); + + app.use(function (req, res, next) { + return router.handle(req, res, next); + }); + + app.get('/', function (req, res, next) { + res.send('yee'); + next(); + }); + + request(app) + .get('/') + .expect('old', 'foo') + .expect(function (res) { + if (typeof res.headers['new'] !== 'undefined') { + throw new Error('`new` header should not be present'); + } + }) + .expect(200, 'yee', function (err, res) { + if (err) return done(err); + + router = new express.Router(); + + router.use(function (req, res, next) { + res.setHeader('new', 'bar'); + next(); + }); + + request(app) + .get('/') + .expect('new', 'bar') + .expect(function (res) { + if (typeof res.headers['old'] !== 'undefined') { + throw new Error('`old` header should not be present'); + } + }) + .expect(200, 'yee', done); + }); }) }) + +function supportsRegexp(source) { + try { + new RegExp(source) + return true + } catch (e) { + return false + } +} diff --git a/test/app.routes.error.js b/test/app.routes.error.js index 56081b31127..ed53c7857ba 100644 --- a/test/app.routes.error.js +++ b/test/app.routes.error.js @@ -1,6 +1,6 @@ 'use strict' -var assert = require('assert') +var assert = require('node:assert') var express = require('../') , request = require('supertest'); @@ -51,7 +51,7 @@ describe('app', function(){ assert.ok(b) assert.ok(c) assert.ok(!d) - res.send(204); + res.sendStatus(204); }); request(app) diff --git a/test/app.use.js b/test/app.use.js index 1de3275c8e2..1d56aa3b029 100644 --- a/test/app.use.js +++ b/test/app.use.js @@ -1,7 +1,7 @@ 'use strict' var after = require('after'); -var assert = require('assert') +var assert = require('node:assert') var express = require('..'); var request = require('supertest'); @@ -258,27 +258,27 @@ describe('app', function(){ describe('.use(path, middleware)', function(){ it('should require middleware', function () { var app = express() - assert.throws(function () { app.use('/') }, /requires a middleware function/) + assert.throws(function () { app.use('/') }, 'TypeError: app.use() requires a middleware function') }) it('should reject string as middleware', function () { var app = express() - assert.throws(function () { app.use('/', 'foo') }, /requires a middleware function but got a string/) + assert.throws(function () { app.use('/', 'foo') }, /argument handler must be a function/) }) it('should reject number as middleware', function () { var app = express() - assert.throws(function () { app.use('/', 42) }, /requires a middleware function but got a number/) + assert.throws(function () { app.use('/', 42) }, /argument handler must be a function/) }) it('should reject null as middleware', function () { var app = express() - assert.throws(function () { app.use('/', null) }, /requires a middleware function but got a Null/) + assert.throws(function () { app.use('/', null) }, /argument handler must be a function/) }) it('should reject Date as middleware', function () { var app = express() - assert.throws(function () { app.use('/', new Date()) }, /requires a middleware function but got a Date/) + assert.throws(function () { app.use('/', new Date()) }, /argument handler must be a function/) }) it('should strip path from req.url', function (done) { diff --git a/test/config.js b/test/config.js index b04367fdbf8..d004de03eaa 100644 --- a/test/config.js +++ b/test/config.js @@ -1,6 +1,6 @@ 'use strict' -var assert = require('assert'); +var assert = require('node:assert'); var express = require('..'); describe('config', function () { diff --git a/test/exports.js b/test/exports.js index 5ab0f885ce4..fc7836c1594 100644 --- a/test/exports.js +++ b/test/exports.js @@ -1,6 +1,6 @@ 'use strict' -var assert = require('assert') +var assert = require('node:assert') var express = require('../'); var request = require('supertest'); @@ -79,9 +79,4 @@ describe('exports', function(){ .get('/') .expect('bar', done); }) - - it('should throw on old middlewares', function(){ - assert.throws(function () { express.bodyParser() }, /Error:.*middleware.*bodyParser/) - assert.throws(function () { express.limit() }, /Error:.*middleware.*limit/) - }) }) diff --git a/test/express.json.js b/test/express.json.js index f6f536b15e5..6b91734ed3b 100644 --- a/test/express.json.js +++ b/test/express.json.js @@ -1,15 +1,11 @@ 'use strict' -var assert = require('assert') -var asyncHooks = tryRequire('async_hooks') -var Buffer = require('safe-buffer').Buffer +var assert = require('node:assert') +var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage + var express = require('..') var request = require('supertest') -var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' - ? describe - : describe.skip - describe('express.json()', function () { it('should parse JSON', function (done) { request(createApp()) @@ -43,12 +39,13 @@ describe('express.json()', function () { .expect(200, '{}', done) }) + // The old node error message modification in body parser is catching this it('should 400 when only whitespace', function (done) { request(createApp()) .post('/') .set('Content-Type', 'application/json') .send(' \n') - .expect(400, '[entity.parse.failed] ' + parseError(' '), done) + .expect(400, '[entity.parse.failed] ' + parseError(' \n'), done) }) it('should 400 when invalid content-length', function (done) { @@ -72,32 +69,6 @@ describe('express.json()', function () { .expect(400, /content length/, done) }) - it('should 500 if stream not readable', function (done) { - var app = express() - - app.use(function (req, res, next) { - req.on('end', next) - req.resume() - }) - - app.use(express.json()) - - app.use(function (err, req, res, next) { - res.status(err.status || 500) - res.send('[' + err.type + '] ' + err.message) - }) - - app.post('/', function (req, res) { - res.json(req.body) - }) - - request(app) - .post('/') - .set('Content-Type', 'application/json') - .send('{"user":"tobi"}') - .expect(500, '[stream.not.readable] stream is not readable', done) - }) - it('should handle duplicated middleware', function (done) { var app = express() @@ -341,7 +312,7 @@ describe('express.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('{"user":"tobi"}') - .expect(200, '{}', done) + .expect(200, '', done) }) }) @@ -373,7 +344,7 @@ describe('express.json()', function () { .post('/') .set('Content-Type', 'application/x-json') .send('{"user":"tobi"}') - .expect(200, '{}', done) + .expect(200, '', done) }) }) @@ -528,13 +499,13 @@ describe('express.json()', function () { }) }) - describeAsyncHooks('async local storage', function () { + describe('async local storage', function () { before(function () { var app = express() var store = { foo: 'bar' } app.use(function (req, res, next) { - req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage = new AsyncLocalStorage() req.asyncLocalStorage.run(store, next) }) @@ -579,14 +550,14 @@ describe('express.json()', function () { .end(done) }) - it('should presist store when unmatched content-type', function (done) { + it('should persist store when unmatched content-type', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/fizzbuzz') .send('buzz') .expect(200) .expect('x-store-foo', 'bar') - .expect('{}') + .expect('') .end(done) }) @@ -753,6 +724,7 @@ function createApp (options) { app.use(express.json(options)) app.use(function (err, req, res, next) { + // console.log(err) res.status(err.status || 500) res.send(String(req.headers['x-error-property'] ? err[req.headers['x-error-property']] @@ -780,11 +752,3 @@ function shouldContainInBody (str) { 'expected \'' + res.text + '\' to contain \'' + str + '\'') } } - -function tryRequire (name) { - try { - return require(name) - } catch (e) { - return {} - } -} diff --git a/test/express.raw.js b/test/express.raw.js index 4aa62bb85bc..362fccb1e38 100644 --- a/test/express.raw.js +++ b/test/express.raw.js @@ -1,15 +1,11 @@ 'use strict' -var assert = require('assert') -var asyncHooks = tryRequire('async_hooks') -var Buffer = require('safe-buffer').Buffer +var assert = require('node:assert') +var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage + var express = require('..') var request = require('supertest') -var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' - ? describe - : describe.skip - describe('express.raw()', function () { before(function () { this.app = createApp() @@ -65,36 +61,6 @@ describe('express.raw()', function () { .expect(200, { buf: '' }, done) }) - it('should 500 if stream not readable', function (done) { - var app = express() - - app.use(function (req, res, next) { - req.on('end', next) - req.resume() - }) - - app.use(express.raw()) - - app.use(function (err, req, res, next) { - res.status(err.status || 500) - res.send('[' + err.type + '] ' + err.message) - }) - - app.post('/', function (req, res) { - if (Buffer.isBuffer(req.body)) { - res.json({ buf: req.body.toString('hex') }) - } else { - res.json(req.body) - } - }) - - request(app) - .post('/') - .set('Content-Type', 'application/octet-stream') - .send('the user is tobi') - .expect(500, '[stream.not.readable] stream is not readable', done) - }) - it('should handle duplicated middleware', function (done) { var app = express() @@ -236,7 +202,7 @@ describe('express.raw()', function () { var test = request(this.app).post('/') test.set('Content-Type', 'application/octet-stream') test.write(Buffer.from('000102', 'hex')) - test.expect(200, '{}', done) + test.expect(200, '', done) }) }) @@ -265,7 +231,7 @@ describe('express.raw()', function () { var test = request(this.app).post('/') test.set('Content-Type', 'application/x-foo') test.write(Buffer.from('000102', 'hex')) - test.expect(200, '{}', done) + test.expect(200, '', done) }) }) @@ -358,13 +324,13 @@ describe('express.raw()', function () { }) }) - describeAsyncHooks('async local storage', function () { + describe('async local storage', function () { before(function () { var app = express() var store = { foo: 'bar' } app.use(function (req, res, next) { - req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage = new AsyncLocalStorage() req.asyncLocalStorage.run(store, next) }) @@ -420,7 +386,6 @@ describe('express.raw()', function () { .send('buzz') .expect(200) .expect('x-store-foo', 'bar') - .expect('{}') .end(done) }) @@ -545,11 +510,3 @@ function createApp (options) { return app } - -function tryRequire (name) { - try { - return require(name) - } catch (e) { - return {} - } -} diff --git a/test/express.static.js b/test/express.static.js index 245fd5929cc..16a8ec0516f 100644 --- a/test/express.static.js +++ b/test/express.static.js @@ -1,9 +1,8 @@ 'use strict' -var assert = require('assert') -var Buffer = require('safe-buffer').Buffer +var assert = require('node:assert') var express = require('..') -var path = require('path') +var path = require('node:path') var request = require('supertest') var utils = require('./support/utils') @@ -41,7 +40,7 @@ describe('express.static()', function () { it('should set Content-Type', function (done) { request(this.app) .get('/todo.txt') - .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect('Content-Type', 'text/plain; charset=utf-8') .expect(200, done) }) @@ -486,7 +485,7 @@ describe('express.static()', function () { request(this.app) .get('/users') .expect('Location', '/users/') - .expect(301, //, done) + .expect(301, /\/users\//, done) }) it('should redirect directories with query string', function (done) { @@ -508,7 +507,7 @@ describe('express.static()', function () { .get('/snow') .expect('Location', '/snow%20%E2%98%83/') .expect('Content-Type', /html/) - .expect(301, />Redirecting to \/snow%20%E2%98%83\/<\/a>Redirecting to \/snow%20%E2%98%83\/tobi') - .expect(200, '{}', done) + .expect(200, '', done) }) }) @@ -387,13 +357,13 @@ describe('express.text()', function () { }) }) - describeAsyncHooks('async local storage', function () { + describe('async local storage', function () { before(function () { var app = express() var store = { foo: 'bar' } app.use(function (req, res, next) { - req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage = new AsyncLocalStorage() req.asyncLocalStorage.run(store, next) }) @@ -445,7 +415,6 @@ describe('express.text()', function () { .send('buzz') .expect(200) .expect('x-store-foo', 'bar') - .expect('{}') .end(done) }) @@ -595,11 +564,3 @@ function createApp (options) { return app } - -function tryRequire (name) { - try { - return require(name) - } catch (e) { - return {} - } -} diff --git a/test/express.urlencoded.js b/test/express.urlencoded.js index e07432c86c3..b2df949f9f3 100644 --- a/test/express.urlencoded.js +++ b/test/express.urlencoded.js @@ -1,15 +1,11 @@ 'use strict' -var assert = require('assert') -var asyncHooks = tryRequire('async_hooks') -var Buffer = require('safe-buffer').Buffer +var assert = require('node:assert') +var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage + var express = require('..') var request = require('supertest') -var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' - ? describe - : describe.skip - describe('express.urlencoded()', function () { before(function () { this.app = createApp() @@ -62,32 +58,6 @@ describe('express.urlencoded()', function () { .expect(200, '{}', done) }) - it('should 500 if stream not readable', function (done) { - var app = express() - - app.use(function (req, res, next) { - req.on('end', next) - req.resume() - }) - - app.use(express.urlencoded()) - - app.use(function (err, req, res, next) { - res.status(err.status || 500) - res.send('[' + err.type + '] ' + err.message) - }) - - app.post('/', function (req, res) { - res.json(req.body) - }) - - request(app) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send('user=tobi') - .expect(500, '[stream.not.readable] stream is not readable', done) - }) - it('should handle duplicated middleware', function (done) { var app = express() @@ -105,12 +75,12 @@ describe('express.urlencoded()', function () { .expect(200, '{"user":"tobi"}', done) }) - it('should parse extended syntax', function (done) { + it('should not parse extended syntax', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user[name][first]=Tobi') - .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) + .expect(200, '{"user[name][first]":"Tobi"}', done) }) describe('with extended option', function () { @@ -212,7 +182,7 @@ describe('express.urlencoded()', function () { it('should parse deep object', function (done) { var str = 'foo' - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 32; i++) { str += '[p]' } @@ -230,7 +200,7 @@ describe('express.urlencoded()', function () { var depth = 0 var ref = obj.foo while ((ref = ref.p)) { depth++ } - assert.strictEqual(depth, 500) + assert.strictEqual(depth, 32) }) .expect(200, done) }) @@ -473,7 +443,7 @@ describe('express.urlencoded()', function () { .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=tobi') - .expect(200, '{}', done) + .expect(200, '', done) }) }) @@ -505,7 +475,7 @@ describe('express.urlencoded()', function () { .post('/') .set('Content-Type', 'application/x-foo') .send('user=tobi') - .expect(200, '{}', done) + .expect(200, '', done) }) }) @@ -632,13 +602,13 @@ describe('express.urlencoded()', function () { }) }) - describeAsyncHooks('async local storage', function () { + describe('async local storage', function () { before(function () { var app = express() var store = { foo: 'bar' } app.use(function (req, res, next) { - req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage = new AsyncLocalStorage() req.asyncLocalStorage.run(store, next) }) @@ -690,7 +660,6 @@ describe('express.urlencoded()', function () { .send('buzz') .expect(200) .expect('x-store-foo', 'bar') - .expect('{}') .end(done) }) @@ -856,11 +825,3 @@ function expectKeyCount (count) { assert.strictEqual(Object.keys(JSON.parse(res.text)).length, count) } } - -function tryRequire (name) { - try { - return require(name) - } catch (e) { - return {} - } -} diff --git a/test/middleware.basic.js b/test/middleware.basic.js index 19f00d9a296..1f1ed17571a 100644 --- a/test/middleware.basic.js +++ b/test/middleware.basic.js @@ -1,6 +1,6 @@ 'use strict' -var assert = require('assert') +var assert = require('node:assert') var express = require('../'); var request = require('supertest'); diff --git a/test/req.acceptsCharset.js b/test/req.acceptsCharset.js deleted file mode 100644 index 6dbab439b7e..00000000000 --- a/test/req.acceptsCharset.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict' - -var express = require('../') - , request = require('supertest'); - -describe('req', function(){ - describe('.acceptsCharset(type)', function(){ - describe('when Accept-Charset is not present', function(){ - it('should return true', function(done){ - var app = express(); - - app.use(function(req, res, next){ - res.end(req.acceptsCharset('utf-8') ? 'yes' : 'no'); - }); - - request(app) - .get('/') - .expect('yes', done); - }) - }) - - describe('when Accept-Charset is present', function () { - it('should return true', function (done) { - var app = express(); - - app.use(function(req, res, next){ - res.end(req.acceptsCharset('utf-8') ? 'yes' : 'no'); - }); - - request(app) - .get('/') - .set('Accept-Charset', 'foo, bar, utf-8') - .expect('yes', done); - }) - - it('should return false otherwise', function(done){ - var app = express(); - - app.use(function(req, res, next){ - res.end(req.acceptsCharset('utf-8') ? 'yes' : 'no'); - }); - - request(app) - .get('/') - .set('Accept-Charset', 'foo, bar') - .expect('no', done); - }) - }) - }) -}) diff --git a/test/req.acceptsEncoding.js b/test/req.acceptsEncoding.js deleted file mode 100644 index bcec2280e65..00000000000 --- a/test/req.acceptsEncoding.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict' - -var express = require('../') - , request = require('supertest'); - -describe('req', function(){ - describe('.acceptsEncoding', function(){ - it('should return encoding if accepted', function (done) { - var app = express(); - - app.get('/', function (req, res) { - res.send({ - gzip: req.acceptsEncoding('gzip'), - deflate: req.acceptsEncoding('deflate') - }) - }) - - request(app) - .get('/') - .set('Accept-Encoding', ' gzip, deflate') - .expect(200, { gzip: 'gzip', deflate: 'deflate' }, done) - }) - - it('should be false if encoding not accepted', function(done){ - var app = express(); - - app.get('/', function (req, res) { - res.send({ - bogus: req.acceptsEncoding('bogus') - }) - }) - - request(app) - .get('/') - .set('Accept-Encoding', ' gzip, deflate') - .expect(200, { bogus: false }, done) - }) - }) -}) diff --git a/test/req.acceptsLanguage.js b/test/req.acceptsLanguage.js deleted file mode 100644 index 39bd73c4834..00000000000 --- a/test/req.acceptsLanguage.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict' - -var express = require('../') - , request = require('supertest'); - -describe('req', function(){ - describe('.acceptsLanguage', function(){ - it('should return language if accepted', function (done) { - var app = express(); - - app.get('/', function (req, res) { - res.send({ - 'en-us': req.acceptsLanguage('en-us'), - en: req.acceptsLanguage('en') - }) - }) - - request(app) - .get('/') - .set('Accept-Language', 'en;q=.5, en-us') - .expect(200, { 'en-us': 'en-us', en: 'en' }, done) - }) - - it('should be false if language not accepted', function(done){ - var app = express(); - - app.get('/', function (req, res) { - res.send({ - es: req.acceptsLanguage('es') - }) - }) - - request(app) - .get('/') - .set('Accept-Language', 'en;q=.5, en-us') - .expect(200, { es: false }, done) - }) - - describe('when Accept-Language is not present', function(){ - it('should always return language', function (done) { - var app = express(); - - app.get('/', function (req, res) { - res.send({ - en: req.acceptsLanguage('en'), - es: req.acceptsLanguage('es'), - jp: req.acceptsLanguage('jp') - }) - }) - - request(app) - .get('/') - .expect(200, { en: 'en', es: 'es', jp: 'jp' }, done) - }) - }) - }) -}) diff --git a/test/req.fresh.js b/test/req.fresh.js index 9160e2caaf6..3bf6a1f65a7 100644 --- a/test/req.fresh.js +++ b/test/req.fresh.js @@ -46,5 +46,25 @@ describe('req', function(){ .get('/') .expect(200, 'false', done); }) + + it('should ignore "If-Modified-Since" when "If-None-Match" is present', function(done) { + var app = express(); + const etag = '"FooBar"' + const now = Date.now() + + app.disable('x-powered-by') + app.use(function(req, res) { + res.set('Etag', etag) + res.set('Last-Modified', new Date(now).toUTCString()) + res.send(req.fresh); + }); + + request(app) + .get('/') + .set('If-Modified-Since', new Date(now - 1000).toUTCString) + .set('If-None-Match', etag) + .expect(304, done); + }) + }) }) diff --git a/test/req.get.js b/test/req.get.js index 16589b3f059..e73d109c84a 100644 --- a/test/req.get.js +++ b/test/req.get.js @@ -2,7 +2,7 @@ var express = require('../') , request = require('supertest') - , assert = require('assert'); + , assert = require('node:assert'); describe('req', function(){ describe('.get(field)', function(){ diff --git a/test/req.host.js b/test/req.host.js index 2c051fb9791..cdda82eaae3 100644 --- a/test/req.host.js +++ b/test/req.host.js @@ -28,7 +28,7 @@ describe('req', function(){ request(app) .post('/') .set('Host', 'example.com:3000') - .expect('example.com', done); + .expect(200, 'example.com:3000', done); }) it('should return undefined otherwise', function(done){ @@ -67,7 +67,7 @@ describe('req', function(){ request(app) .post('/') .set('Host', '[::1]:3000') - .expect('[::1]', done); + .expect(200, '[::1]:3000', done); }) describe('when "trust proxy" is enabled', function(){ diff --git a/test/req.param.js b/test/req.param.js deleted file mode 100644 index b3748c02bce..00000000000 --- a/test/req.param.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict' - -var express = require('../') - , request = require('supertest') - -describe('req', function(){ - describe('.param(name, default)', function(){ - it('should use the default value unless defined', function(done){ - var app = express(); - - app.use(function(req, res){ - res.end(req.param('name', 'tj')); - }); - - request(app) - .get('/') - .expect('tj', done); - }) - }) - - describe('.param(name)', function(){ - it('should check req.query', function(done){ - var app = express(); - - app.use(function(req, res){ - res.end(req.param('name')); - }); - - request(app) - .get('/?name=tj') - .expect('tj', done); - }) - - it('should check req.body', function(done){ - var app = express(); - - app.use(express.json()) - - app.use(function(req, res){ - res.end(req.param('name')); - }); - - request(app) - .post('/') - .send({ name: 'tj' }) - .expect('tj', done); - }) - - it('should check req.params', function(done){ - var app = express(); - - app.get('/user/:name', function(req, res){ - res.end(req.param('filter') + req.param('name')); - }); - - request(app) - .get('/user/tj') - .expect('undefinedtj', done); - }) - }) -}) diff --git a/test/req.query.js b/test/req.query.js index 6fae592dccc..c0d3c8376e9 100644 --- a/test/req.query.js +++ b/test/req.query.js @@ -1,6 +1,6 @@ 'use strict' -var assert = require('assert') +var assert = require('node:assert') var express = require('../') , request = require('supertest'); @@ -14,12 +14,12 @@ describe('req', function(){ .expect(200, '{}', done); }); - it('should default to parse complex keys', function (done) { + it('should default to parse simple keys', function (done) { var app = createApp(); request(app) .get('/?user[name]=tj') - .expect(200, '{"user":{"name":"tj"}}', done); + .expect(200, '{"user[name]":"tj"}', done); }); describe('when "query parser" is extended', function () { @@ -82,23 +82,6 @@ describe('req', function(){ }); }); - describe('when "query parser fn" is missing', function () { - it('should act like "extended"', function (done) { - var app = express(); - - delete app.settings['query parser']; - delete app.settings['query parser fn']; - - app.use(function (req, res) { - res.send(req.query); - }); - - request(app) - .get('/?user[name]=tj&user.name=tj') - .expect(200, '{"user":{"name":"tj"},"user.name":"tj"}', done); - }); - }); - describe('when "query parser" an unknown value', function () { it('should throw', function () { assert.throws(createApp.bind(null, 'bogus'), diff --git a/test/req.route.js b/test/req.route.js index 6c17fbb1c8f..9bd7ed923b4 100644 --- a/test/req.route.js +++ b/test/req.route.js @@ -8,7 +8,7 @@ describe('req', function(){ it('should be the executed Route', function(done){ var app = express(); - app.get('/user/:id/:op?', function(req, res, next){ + app.get('/user/:id{/:op}', function(req, res, next){ res.header('path-1', req.route.path) next(); }); @@ -20,7 +20,7 @@ describe('req', function(){ request(app) .get('/user/12/edit') - .expect('path-1', '/user/:id/:op?') + .expect('path-1', '/user/:id{/:op}') .expect('path-2', '/user/:id/edit') .expect(200, done) }) diff --git a/test/res.append.js b/test/res.append.js index 8f72598bf52..325dd4d12e0 100644 --- a/test/res.append.js +++ b/test/res.append.js @@ -1,6 +1,6 @@ 'use strict' -var assert = require('assert') +var assert = require('node:assert') var express = require('..') var request = require('supertest') diff --git a/test/res.attachment.js b/test/res.attachment.js index 6283ded0d65..68e611bbc79 100644 --- a/test/res.attachment.js +++ b/test/res.attachment.js @@ -1,6 +1,5 @@ 'use strict' -var Buffer = require('safe-buffer').Buffer var express = require('../') , request = require('supertest'); diff --git a/test/res.clearCookie.js b/test/res.clearCookie.js index fc0cfb99a3d..74a746eb7be 100644 --- a/test/res.clearCookie.js +++ b/test/res.clearCookie.js @@ -32,5 +32,31 @@ describe('res', function(){ .expect('Set-Cookie', 'sid=; Path=/admin; Expires=Thu, 01 Jan 1970 00:00:00 GMT') .expect(200, done) }) + + it('should ignore maxAge', function(done){ + var app = express(); + + app.use(function(req, res){ + res.clearCookie('sid', { path: '/admin', maxAge: 1000 }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'sid=; Path=/admin; Expires=Thu, 01 Jan 1970 00:00:00 GMT') + .expect(200, done) + }) + + it('should ignore user supplied expires param', function(done){ + var app = express(); + + app.use(function(req, res){ + res.clearCookie('sid', { path: '/admin', expires: new Date() }).end(); + }); + + request(app) + .get('/') + .expect('Set-Cookie', 'sid=; Path=/admin; Expires=Thu, 01 Jan 1970 00:00:00 GMT') + .expect(200, done) + }) }) }) diff --git a/test/res.cookie.js b/test/res.cookie.js index c837820605c..180d1be3452 100644 --- a/test/res.cookie.js +++ b/test/res.cookie.js @@ -3,7 +3,6 @@ var express = require('../') , request = require('supertest') , cookieParser = require('cookie-parser') -var merge = require('utils-merge'); describe('res', function(){ describe('.cookie(name, object)', function(){ @@ -130,7 +129,7 @@ describe('res', function(){ var app = express(); var options = { maxAge: 1000 }; - var optionsCopy = merge({}, options); + var optionsCopy = { ...options }; app.use(function(req, res){ res.cookie('name', 'tobi', options) diff --git a/test/res.download.js b/test/res.download.js index b52e66803c6..1bd7663c549 100644 --- a/test/res.download.js +++ b/test/res.download.js @@ -1,20 +1,16 @@ 'use strict' var after = require('after'); -var assert = require('assert') -var asyncHooks = tryRequire('async_hooks') -var Buffer = require('safe-buffer').Buffer +var assert = require('node:assert') +var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage + var express = require('..'); -var path = require('path') +var path = require('node:path') var request = require('supertest'); var utils = require('./support/utils') var FIXTURES_PATH = path.join(__dirname, 'fixtures') -var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' - ? describe - : describe.skip - describe('res', function(){ describe('.download(path)', function(){ it('should transfer as an attachment', function(done){ @@ -26,7 +22,7 @@ describe('res', function(){ request(app) .get('/') - .expect('Content-Type', 'text/html; charset=UTF-8') + .expect('Content-Type', 'text/html; charset=utf-8') .expect('Content-Disposition', 'attachment; filename="user.html"') .expect(200, '

{{user.name}}

', done) }) @@ -69,7 +65,7 @@ describe('res', function(){ request(app) .get('/') - .expect('Content-Type', 'text/html; charset=UTF-8') + .expect('Content-Type', 'text/html; charset=utf-8') .expect('Content-Disposition', 'attachment; filename="document"') .expect(200, done) }) @@ -86,19 +82,19 @@ describe('res', function(){ request(app) .get('/') - .expect('Content-Type', 'text/html; charset=UTF-8') + .expect('Content-Type', 'text/html; charset=utf-8') .expect('Content-Disposition', 'attachment; filename="user.html"') .expect(200, cb); }) - describeAsyncHooks('async local storage', function () { + describe('async local storage', function () { it('should presist store', function (done) { var app = express() var cb = after(2, done) var store = { foo: 'bar' } app.use(function (req, res, next) { - req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage = new AsyncLocalStorage() req.asyncLocalStorage.run(store, next) }) @@ -115,7 +111,7 @@ describe('res', function(){ request(app) .get('/') - .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Content-Disposition', 'attachment; filename="name.txt"') .expect(200, 'tobi', cb) }) @@ -125,7 +121,7 @@ describe('res', function(){ var store = { foo: 'bar' } app.use(function (req, res, next) { - req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage = new AsyncLocalStorage() req.asyncLocalStorage.run(store, next) }) @@ -369,7 +365,7 @@ describe('res', function(){ request(app) .get('/') - .expect('Content-Type', 'text/html; charset=UTF-8') + .expect('Content-Type', 'text/html; charset=utf-8') .expect('Content-Disposition', 'attachment; filename="document"') .expect(200, cb); }) @@ -388,7 +384,7 @@ describe('res', function(){ request(app) .get('/') .expect(200) - .expect('Content-Type', 'text/html; charset=UTF-8') + .expect('Content-Type', 'text/html; charset=utf-8') .expect('Content-Disposition', 'attachment; filename="document"') .end(cb) }) @@ -488,11 +484,3 @@ describe('res', function(){ }) }) }) - -function tryRequire (name) { - try { - return require(name) - } catch (e) { - return {} - } -} diff --git a/test/res.format.js b/test/res.format.js index cba6fe136b1..be427309577 100644 --- a/test/res.format.js +++ b/test/res.format.js @@ -3,7 +3,7 @@ var after = require('after') var express = require('../') , request = require('supertest') - , assert = require('assert'); + , assert = require('node:assert'); var app1 = express(); @@ -28,7 +28,8 @@ app1.use(function(req, res, next){ app1.use(function(err, req, res, next){ if (!err.types) throw err; - res.send(err.status, 'Supports: ' + err.types.join(', ')); + res.status(err.status) + res.send('Supports: ' + err.types.join(', ')) }) var app2 = express(); @@ -42,7 +43,8 @@ app2.use(function(req, res, next){ }); app2.use(function(err, req, res, next){ - res.send(err.status, 'Supports: ' + err.types.join(', ')); + res.status(err.status) + res.send('Supports: ' + err.types.join(', ')) }) var app3 = express(); @@ -70,7 +72,8 @@ app4.get('/', function (req, res) { }); app4.use(function(err, req, res, next){ - res.send(err.status, 'Supports: ' + err.types.join(', ')); + res.status(err.status) + res.send('Supports: ' + err.types.join(', ')) }) var app5 = express(); @@ -103,7 +106,8 @@ describe('res', function(){ }); app.use(function(err, req, res, next){ - res.send(err.status, 'Supports: ' + err.types.join(', ')); + res.status(err.status) + res.send('Supports: ' + err.types.join(', ')) }); test(app); @@ -164,7 +168,8 @@ describe('res', function(){ }); router.use(function(err, req, res, next){ - res.send(err.status, 'Supports: ' + err.types.join(', ')); + res.status(err.status) + res.send('Supports: ' + err.types.join(', ')) }) app.use(router) diff --git a/test/res.json.js b/test/res.json.js index dcaceae5ca4..ffd547e95b2 100644 --- a/test/res.json.js +++ b/test/res.json.js @@ -2,7 +2,7 @@ var express = require('../') , request = require('supertest') - , assert = require('assert'); + , assert = require('node:assert'); describe('res', function(){ describe('.json(object)', function(){ @@ -183,47 +183,4 @@ describe('res', function(){ }) }) }) - - describe('.json(status, object)', function(){ - it('should respond with json and set the .statusCode', function(done){ - var app = express(); - - app.use(function(req, res){ - res.json(201, { id: 1 }); - }); - - request(app) - .get('/') - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(201, '{"id":1}', done) - }) - }) - - describe('.json(object, status)', function(){ - it('should respond with json and set the .statusCode for backwards compat', function(done){ - var app = express(); - - app.use(function(req, res){ - res.json({ id: 1 }, 201); - }); - - request(app) - .get('/') - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(201, '{"id":1}', done) - }) - - it('should use status as second number for backwards compat', function(done){ - var app = express(); - - app.use(function(req, res){ - res.json(200, 201); - }); - - request(app) - .get('/') - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(201, '200', done) - }) - }) }) diff --git a/test/res.jsonp.js b/test/res.jsonp.js index 0735d43bd50..c1f90f11092 100644 --- a/test/res.jsonp.js +++ b/test/res.jsonp.js @@ -2,7 +2,7 @@ var express = require('../') , request = require('supertest') - , assert = require('assert'); + , assert = require('node:assert'); var utils = require('./support/utils'); describe('res', function(){ @@ -328,49 +328,6 @@ describe('res', function(){ }) }) - describe('.jsonp(status, object)', function(){ - it('should respond with json and set the .statusCode', function(done){ - var app = express(); - - app.use(function(req, res){ - res.jsonp(201, { id: 1 }); - }); - - request(app) - .get('/') - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(201, '{"id":1}', done) - }) - }) - - describe('.jsonp(object, status)', function(){ - it('should respond with json and set the .statusCode for backwards compat', function(done){ - var app = express(); - - app.use(function(req, res){ - res.jsonp({ id: 1 }, 201); - }); - - request(app) - .get('/') - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(201, '{"id":1}', done) - }) - - it('should use status as second number for backwards compat', function(done){ - var app = express(); - - app.use(function(req, res){ - res.jsonp(200, 201); - }); - - request(app) - .get('/') - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(201, '200', done) - }) - }) - it('should not override previous Content-Types', function(done){ var app = express(); diff --git a/test/res.links.js b/test/res.links.js index 240b7fcfda3..40665fd558a 100644 --- a/test/res.links.js +++ b/test/res.links.js @@ -43,5 +43,23 @@ describe('res', function(){ .expect('Link', '; rel="next", ; rel="last", ; rel="prev"') .expect(200, done); }) + + it('should set multiple links for single rel', function (done) { + var app = express(); + + app.use(function (req, res) { + res.links({ + next: 'http://api.example.com/users?page=2', + last: ['http://api.example.com/users?page=5', 'http://api.example.com/users?page=1'] + }); + + res.end(); + }); + + request(app) + .get('/') + .expect('Link', '; rel="next", ; rel="last", ; rel="last"') + .expect(200, done); + }) }) }) diff --git a/test/res.location.js b/test/res.location.js index c80b38de6b8..b81c6f07d8d 100644 --- a/test/res.location.js +++ b/test/res.location.js @@ -2,8 +2,8 @@ var express = require('../') , request = require('supertest') - , assert = require('assert') - , url = require('url'); + , assert = require('node:assert') + , url = require('node:url'); describe('res', function(){ describe('.location(url)', function(){ @@ -46,65 +46,7 @@ describe('res', function(){ .expect(200, done) }) - describe('when url is "back"', function () { - it('should set location from "Referer" header', function (done) { - var app = express() - - app.use(function (req, res) { - res.location('back').end() - }) - - request(app) - .get('/') - .set('Referer', '/some/page.html') - .expect('Location', '/some/page.html') - .expect(200, done) - }) - - it('should set location from "Referrer" header', function (done) { - var app = express() - - app.use(function (req, res) { - res.location('back').end() - }) - - request(app) - .get('/') - .set('Referrer', '/some/page.html') - .expect('Location', '/some/page.html') - .expect(200, done) - }) - - it('should prefer "Referrer" header', function (done) { - var app = express() - - app.use(function (req, res) { - res.location('back').end() - }) - - request(app) - .get('/') - .set('Referer', '/some/page1.html') - .set('Referrer', '/some/page2.html') - .expect('Location', '/some/page2.html') - .expect(200, done) - }) - - it('should set the header to "/" without referrer', function (done) { - var app = express() - - app.use(function (req, res) { - res.location('back').end() - }) - - request(app) - .get('/') - .expect('Location', '/') - .expect(200, done) - }) - }) - - it('should encode data uri', function (done) { + it('should encode data uri1', function (done) { var app = express() app.use(function (req, res) { res.location('data:text/javascript,export default () => { }').end(); @@ -116,7 +58,7 @@ describe('res', function(){ .expect(200, done) }) - it('should encode data uri', function (done) { + it('should encode data uri2', function (done) { var app = express() app.use(function (req, res) { res.location('data:text/javascript,export default () => { }').end(); @@ -305,23 +247,12 @@ describe('res', function(){ ); }); - it('should percent encode backslashes in the path', function (done) { + it('should keep backslashes in the path', function (done) { var app = createRedirectServerForDomain('google.com'); testRequestedRedirect( app, 'https://google.com/foo\\bar\\baz', - 'https://google.com/foo%5Cbar%5Cbaz', - 'google.com', - done - ); - }); - - it('should encode backslashes in the path after the first backslash that triggered path parsing', function (done) { - var app = createRedirectServerForDomain('google.com'); - testRequestedRedirect( - app, - 'https://google.com\\@app\\l\\e.com', - 'https://google.com\\@app%5Cl%5Ce.com', + 'https://google.com/foo\\bar\\baz', 'google.com', done ); @@ -376,7 +307,7 @@ describe('res', function(){ testRequestedRedirect( app, 'file:///etc\\passwd', - 'file:///etc%5Cpasswd', + 'file:///etc\\passwd', '', done ); diff --git a/test/res.redirect.js b/test/res.redirect.js index 5ffc7e48f12..264e0f2b8f3 100644 --- a/test/res.redirect.js +++ b/test/res.redirect.js @@ -61,21 +61,6 @@ describe('res', function(){ }) }) - describe('.redirect(url, status)', function(){ - it('should set the response status', function(done){ - var app = express(); - - app.use(function(req, res){ - res.redirect('http://google.com', 303); - }); - - request(app) - .get('/') - .expect('Location', 'http://google.com') - .expect(303, done) - }) - }) - describe('when the request method is HEAD', function(){ it('should ignore the body', function(done){ var app = express(); @@ -106,7 +91,7 @@ describe('res', function(){ .set('Accept', 'text/html') .expect('Content-Type', /html/) .expect('Location', 'http://google.com') - .expect(302, '

Found. Redirecting to http://google.com

', done) + .expect(302, '

Found. Redirecting to http://google.com

', done) }) it('should escape the url', function(done){ @@ -122,9 +107,27 @@ describe('res', function(){ .set('Accept', 'text/html') .expect('Content-Type', /html/) .expect('Location', '%3Cla\'me%3E') - .expect(302, '

Found. Redirecting to %3Cla'me%3E

', done) + .expect(302, '

Found. Redirecting to %3Cla'me%3E

', done) }) + it('should not render evil javascript links in anchor href (prevent XSS)', function(done){ + var app = express(); + var xss = 'javascript:eval(document.body.innerHTML=`

XSS

`);'; + var encodedXss = 'javascript:eval(document.body.innerHTML=%60%3Cp%3EXSS%3C/p%3E%60);'; + + app.use(function(req, res){ + res.redirect(xss); + }); + + request(app) + .get('/') + .set('Host', 'http://example.com') + .set('Accept', 'text/html') + .expect('Content-Type', /html/) + .expect('Location', encodedXss) + .expect(302, '

Found. Redirecting to ' + encodedXss +'

', done); + }); + it('should include the redirect type', function(done){ var app = express(); @@ -137,7 +140,7 @@ describe('res', function(){ .set('Accept', 'text/html') .expect('Content-Type', /html/) .expect('Location', 'http://google.com') - .expect(301, '

Moved Permanently. Redirecting to http://google.com

', done); + .expect(301, '

Moved Permanently. Redirecting to http://google.com

', done); }) }) diff --git a/test/res.render.js b/test/res.render.js index 50f0b0a7425..114b398e0b4 100644 --- a/test/res.render.js +++ b/test/res.render.js @@ -1,7 +1,7 @@ 'use strict' var express = require('..'); -var path = require('path') +var path = require('node:path') var request = require('supertest'); var tmpl = require('./support/tmpl'); diff --git a/test/res.send.js b/test/res.send.js index c92568db6ad..78a69a5c666 100644 --- a/test/res.send.js +++ b/test/res.send.js @@ -1,12 +1,13 @@ 'use strict' -var assert = require('assert') -var Buffer = require('safe-buffer').Buffer +var assert = require('node:assert') var express = require('..'); -var methods = require('methods'); +var methods = require('../lib/utils').methods; var request = require('supertest'); var utils = require('./support/utils'); +var shouldSkipQuery = require('./support/utils').shouldSkipQuery + describe('res', function(){ describe('.send()', function(){ it('should set body to ""', function(done){ @@ -51,63 +52,18 @@ describe('res', function(){ }) }) - describe('.send(code)', function(){ - it('should set .statusCode', function(done){ - var app = express(); - - app.use(function(req, res){ - res.send(201) - }); - - request(app) - .get('/') - .expect('Created') - .expect(201, done); - }) - }) - - describe('.send(code, body)', function(){ - it('should set .statusCode and body', function(done){ - var app = express(); - - app.use(function(req, res){ - res.send(201, 'Created :)'); - }); - - request(app) - .get('/') - .expect('Created :)') - .expect(201, done); - }) - }) - - describe('.send(body, code)', function(){ - it('should be supported for backwards compat', function(done){ - var app = express(); - - app.use(function(req, res){ - res.send('Bad!', 400); - }); - - request(app) - .get('/') - .expect('Bad!') - .expect(400, done); - }) - }) - - describe('.send(code, number)', function(){ - it('should send number as json', function(done){ + describe('.send(Number)', function(){ + it('should send as application/json', function(done){ var app = express(); app.use(function(req, res){ - res.send(200, 0.123); + res.send(1000); }); request(app) .get('/') .expect('Content-Type', 'application/json; charset=utf-8') - .expect(200, '0.123', done); + .expect(200, '1000', done) }) }) @@ -221,6 +177,19 @@ describe('res', function(){ .expect(200, 'hey', done); }) + it('should accept Uint8Array', function(done){ + var app = express(); + app.use(function(req, res){ + const encodedHey = new TextEncoder().encode("hey"); + res.set("Content-Type", "text/plain").send(encodedHey); + }) + + request(app) + .get("/") + .expect("Content-Type", "text/plain; charset=utf-8") + .expect(200, "hey", done); + }) + it('should not override ETag', function (done) { var app = express() @@ -409,6 +378,9 @@ describe('res', function(){ if (method === 'connect') return; it('should send ETag in response to ' + method.toUpperCase() + ' request', function (done) { + if (method === 'query' && shouldSkipQuery(process.versions.node)) { + this.skip() + } var app = express(); app[method]('/', function (req, res) { @@ -458,7 +430,7 @@ describe('res', function(){ app.use(function (req, res) { res.set('etag', '"asdf"'); - res.send(200); + res.send('hello!'); }); app.enable('etag'); @@ -509,7 +481,7 @@ describe('res', function(){ app.use(function (req, res) { res.set('etag', '"asdf"'); - res.send(200); + res.send('hello!'); }); request(app) diff --git a/test/res.sendFile.js b/test/res.sendFile.js index 4db0a3b6a4e..63ad5558b57 100644 --- a/test/res.sendFile.js +++ b/test/res.sendFile.js @@ -1,20 +1,16 @@ 'use strict' var after = require('after'); -var asyncHooks = tryRequire('async_hooks') -var Buffer = require('safe-buffer').Buffer +var assert = require('node:assert') +var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage + var express = require('../') , request = require('supertest') - , assert = require('assert'); var onFinished = require('on-finished'); -var path = require('path'); +var path = require('node:path'); var fixtures = path.join(__dirname, 'fixtures'); var utils = require('./support/utils'); -var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' - ? describe - : describe.skip - describe('res', function(){ describe('.sendFile(path)', function () { it('should error missing path', function (done) { @@ -82,6 +78,19 @@ describe('res', function(){ }); }); + it('should disable the ETag function if requested', function (done) { + var app = createApp(path.resolve(fixtures, 'name.txt')).disable('etag'); + + request(app) + .get('/') + .expect(handleHeaders) + .expect(200, done); + + function handleHeaders (res) { + assert(res.headers.etag === undefined); + } + }); + it('should 404 for directory', function (done) { var app = createApp(path.resolve(fixtures, 'blog')); @@ -267,14 +276,14 @@ describe('res', function(){ .expect(200, 'got 404 error', done) }) - describeAsyncHooks('async local storage', function () { + describe('async local storage', function () { it('should presist store', function (done) { var app = express() var cb = after(2, done) var store = { foo: 'bar' } app.use(function (req, res, next) { - req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage = new AsyncLocalStorage() req.asyncLocalStorage.run(store, next) }) @@ -291,7 +300,7 @@ describe('res', function(){ request(app) .get('/') - .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect('Content-Type', 'text/plain; charset=utf-8') .expect(200, 'tobi', cb) }) @@ -300,7 +309,7 @@ describe('res', function(){ var store = { foo: 'bar' } app.use(function (req, res, next) { - req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage = new AsyncLocalStorage() req.asyncLocalStorage.run(store, next) }) @@ -890,507 +899,6 @@ describe('res', function(){ }) }) }) - - describe('.sendfile(path, fn)', function(){ - it('should invoke the callback when complete', function(done){ - var app = express(); - var cb = after(2, done); - - app.use(function(req, res){ - res.sendfile('test/fixtures/user.html', cb) - }); - - request(app) - .get('/') - .expect(200, cb); - }) - - it('should utilize the same options as express.static()', function(done){ - var app = express(); - - app.use(function(req, res){ - res.sendfile('test/fixtures/user.html', { maxAge: 60000 }); - }); - - request(app) - .get('/') - .expect('Cache-Control', 'public, max-age=60') - .end(done); - }) - - it('should invoke the callback when client aborts', function (done) { - var cb = after(2, done) - var app = express(); - - app.use(function (req, res) { - setImmediate(function () { - res.sendfile('test/fixtures/name.txt', function (err) { - assert.ok(err) - assert.strictEqual(err.code, 'ECONNABORTED') - cb() - }); - }); - test.req.abort() - }); - - var server = app.listen() - var test = request(server).get('/') - test.end(function (err) { - assert.ok(err) - server.close(cb) - }) - }) - - it('should invoke the callback when client already aborted', function (done) { - var cb = after(2, done) - var app = express(); - - app.use(function (req, res) { - onFinished(res, function () { - res.sendfile('test/fixtures/name.txt', function (err) { - assert.ok(err) - assert.strictEqual(err.code, 'ECONNABORTED') - cb() - }); - }); - test.req.abort() - }); - - var server = app.listen() - var test = request(server).get('/') - test.end(function (err) { - assert.ok(err) - server.close(cb) - }) - }) - - it('should invoke the callback without error when HEAD', function (done) { - var app = express(); - var cb = after(2, done); - - app.use(function (req, res) { - res.sendfile('test/fixtures/name.txt', cb); - }); - - request(app) - .head('/') - .expect(200, cb); - }); - - it('should invoke the callback without error when 304', function (done) { - var app = express(); - var cb = after(3, done); - - app.use(function (req, res) { - res.sendfile('test/fixtures/name.txt', cb); - }); - - request(app) - .get('/') - .expect('ETag', /^(?:W\/)?"[^"]+"$/) - .expect(200, 'tobi', function (err, res) { - if (err) return cb(err); - var etag = res.headers.etag; - request(app) - .get('/') - .set('If-None-Match', etag) - .expect(304, cb); - }); - }); - - it('should invoke the callback on 404', function(done){ - var app = express(); - var calls = 0; - - app.use(function(req, res){ - res.sendfile('test/fixtures/nope.html', function(err){ - assert.equal(calls++, 0); - assert(!res.headersSent); - res.send(err.message); - }); - }); - - request(app) - .get('/') - .expect(200, /^ENOENT.*?, stat/, done); - }) - - it('should not override manual content-types', function(done){ - var app = express(); - - app.use(function(req, res){ - res.contentType('txt'); - res.sendfile('test/fixtures/user.html'); - }); - - request(app) - .get('/') - .expect('Content-Type', 'text/plain; charset=utf-8') - .end(done); - }) - - it('should invoke the callback on 403', function(done){ - var app = express() - - app.use(function(req, res){ - res.sendfile('test/fixtures/foo/../user.html', function(err){ - assert(!res.headersSent); - res.send(err.message); - }); - }); - - request(app) - .get('/') - .expect('Forbidden') - .expect(200, done); - }) - - it('should invoke the callback on socket error', function(done){ - var app = express() - - app.use(function(req, res){ - res.sendfile('test/fixtures/user.html', function(err){ - assert.ok(err) - assert.ok(!res.headersSent) - assert.strictEqual(err.message, 'broken!') - done(); - }); - - req.socket.destroy(new Error('broken!')) - }); - - request(app) - .get('/') - .end(function(){}); - }) - - describeAsyncHooks('async local storage', function () { - it('should presist store', function (done) { - var app = express() - var cb = after(2, done) - var store = { foo: 'bar' } - - app.use(function (req, res, next) { - req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() - req.asyncLocalStorage.run(store, next) - }) - - app.use(function (req, res) { - res.sendfile('test/fixtures/name.txt', function (err) { - if (err) return cb(err) - - var local = req.asyncLocalStorage.getStore() - - assert.strictEqual(local.foo, 'bar') - cb() - }) - }) - - request(app) - .get('/') - .expect('Content-Type', 'text/plain; charset=UTF-8') - .expect(200, 'tobi', cb) - }) - - it('should presist store on error', function (done) { - var app = express() - var store = { foo: 'bar' } - - app.use(function (req, res, next) { - req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() - req.asyncLocalStorage.run(store, next) - }) - - app.use(function (req, res) { - res.sendfile('test/fixtures/does-not-exist', function (err) { - var local = req.asyncLocalStorage.getStore() - - if (local) { - res.setHeader('x-store-foo', String(local.foo)) - } - - res.send(err ? 'got ' + err.status + ' error' : 'no error') - }) - }) - - request(app) - .get('/') - .expect(200) - .expect('x-store-foo', 'bar') - .expect('got 404 error') - .end(done) - }) - }) - }) - - describe('.sendfile(path)', function(){ - it('should not serve dotfiles', function(done){ - var app = express(); - - app.use(function(req, res){ - res.sendfile('test/fixtures/.name'); - }); - - request(app) - .get('/') - .expect(404, done); - }) - - it('should accept dotfiles option', function(done){ - var app = express(); - - app.use(function(req, res){ - res.sendfile('test/fixtures/.name', { dotfiles: 'allow' }); - }); - - request(app) - .get('/') - .expect(200) - .expect(utils.shouldHaveBody(Buffer.from('tobi'))) - .end(done) - }) - - it('should accept headers option', function(done){ - var app = express(); - var headers = { - 'x-success': 'sent', - 'x-other': 'done' - }; - - app.use(function(req, res){ - res.sendfile('test/fixtures/user.html', { headers: headers }); - }); - - request(app) - .get('/') - .expect('x-success', 'sent') - .expect('x-other', 'done') - .expect(200, done); - }) - - it('should ignore headers option on 404', function(done){ - var app = express(); - var headers = { 'x-success': 'sent' }; - - app.use(function(req, res){ - res.sendfile('test/fixtures/user.nothing', { headers: headers }); - }); - - request(app) - .get('/') - .expect(utils.shouldNotHaveHeader('X-Success')) - .expect(404, done); - }) - - it('should transfer a file', function (done) { - var app = express(); - - app.use(function (req, res) { - res.sendfile('test/fixtures/name.txt'); - }); - - request(app) - .get('/') - .expect(200, 'tobi', done); - }); - - it('should transfer a directory index file', function (done) { - var app = express(); - - app.use(function (req, res) { - res.sendfile('test/fixtures/blog/'); - }); - - request(app) - .get('/') - .expect(200, 'index', done); - }); - - it('should 404 for directory without trailing slash', function (done) { - var app = express(); - - app.use(function (req, res) { - res.sendfile('test/fixtures/blog'); - }); - - request(app) - .get('/') - .expect(404, done); - }); - - it('should transfer a file with urlencoded name', function (done) { - var app = express(); - - app.use(function (req, res) { - res.sendfile('test/fixtures/%25%20of%20dogs.txt'); - }); - - request(app) - .get('/') - .expect(200, '20%', done); - }); - - it('should not error if the client aborts', function (done) { - var app = express(); - var cb = after(2, done) - var error = null - - app.use(function (req, res) { - setImmediate(function () { - res.sendfile(path.resolve(fixtures, 'name.txt')); - setTimeout(function () { - cb(error) - }, 10) - }); - test.req.abort() - }); - - app.use(function (err, req, res, next) { - error = err - next(err) - }); - - var server = app.listen() - var test = request(server).get('/') - test.end(function (err) { - assert.ok(err) - server.close(cb) - }) - }) - - describe('with an absolute path', function(){ - it('should transfer the file', function(done){ - var app = express(); - - app.use(function(req, res){ - res.sendfile(path.join(__dirname, '/fixtures/user.html')) - }); - - request(app) - .get('/') - .expect('Content-Type', 'text/html; charset=UTF-8') - .expect(200, '

{{user.name}}

', done); - }) - }) - - describe('with a relative path', function(){ - it('should transfer the file', function(done){ - var app = express(); - - app.use(function(req, res){ - res.sendfile('test/fixtures/user.html'); - }); - - request(app) - .get('/') - .expect('Content-Type', 'text/html; charset=UTF-8') - .expect(200, '

{{user.name}}

', done); - }) - - it('should serve relative to "root"', function(done){ - var app = express(); - - app.use(function(req, res){ - res.sendfile('user.html', { root: 'test/fixtures/' }); - }); - - request(app) - .get('/') - .expect('Content-Type', 'text/html; charset=UTF-8') - .expect(200, '

{{user.name}}

', done); - }) - - it('should consider ../ malicious when "root" is not set', function(done){ - var app = express(); - - app.use(function(req, res){ - res.sendfile('test/fixtures/foo/../user.html'); - }); - - request(app) - .get('/') - .expect(403, done); - }) - - it('should allow ../ when "root" is set', function(done){ - var app = express(); - - app.use(function(req, res){ - res.sendfile('foo/../user.html', { root: 'test/fixtures' }); - }); - - request(app) - .get('/') - .expect(200, done); - }) - - it('should disallow requesting out of "root"', function(done){ - var app = express(); - - app.use(function(req, res){ - res.sendfile('foo/../../user.html', { root: 'test/fixtures' }); - }); - - request(app) - .get('/') - .expect(403, done); - }) - - it('should next(404) when not found', function(done){ - var app = express() - , calls = 0; - - app.use(function(req, res){ - res.sendfile('user.html'); - }); - - app.use(function(req, res){ - assert(0, 'this should not be called'); - }); - - app.use(function(err, req, res, next){ - ++calls; - next(err); - }); - - request(app) - .get('/') - .expect(404, function (err) { - if (err) return done(err) - assert.strictEqual(calls, 1) - done() - }) - }) - - describe('with non-GET', function(){ - it('should still serve', function(done){ - var app = express() - - app.use(function(req, res){ - res.sendfile(path.join(__dirname, '/fixtures/name.txt')) - }); - - request(app) - .get('/') - .expect('tobi', done); - }) - }) - }) - }) - - describe('.sendfile(path, options)', function () { - it('should pass options to send module', function (done) { - var app = express() - - app.use(function (req, res) { - res.sendfile(path.resolve(fixtures, 'name.txt'), { start: 0, end: 1 }) - }) - - request(app) - .get('/') - .expect(200, 'to', done) - }) - }) }) function createApp(path, options, fn) { @@ -1402,11 +910,3 @@ function createApp(path, options, fn) { return app; } - -function tryRequire (name) { - try { - return require(name) - } catch (e) { - return {} - } -} diff --git a/test/res.sendStatus.js b/test/res.sendStatus.js index 9b1de8385cd..b244cf9d173 100644 --- a/test/res.sendStatus.js +++ b/test/res.sendStatus.js @@ -28,5 +28,17 @@ describe('res', function () { .get('/') .expect(599, '599', done); }) + + it('should raise error for invalid status code', function (done) { + var app = express() + + app.use(function (req, res) { + res.sendStatus(undefined).end() + }) + + request(app) + .get('/') + .expect(500, /TypeError: Invalid status code/, done) + }) }) }) diff --git a/test/res.status.js b/test/res.status.js index 1fe08344eaa..59c8a57e702 100644 --- a/test/res.status.js +++ b/test/res.status.js @@ -1,55 +1,36 @@ 'use strict' - -var express = require('../') -var request = require('supertest') - -var isIoJs = process.release - ? process.release.name === 'io.js' - : ['v1.', 'v2.', 'v3.'].indexOf(process.version.slice(0, 3)) !== -1 +const express = require('../.'); +const request = require('supertest'); describe('res', function () { describe('.status(code)', function () { - describe('when "code" is undefined', function () { - it('should raise error for invalid status code', function (done) { - var app = express() - app.use(function (req, res) { - res.status(undefined).end() - }) + it('should set the status code when valid', function (done) { + var app = express(); - request(app) - .get('/') - .expect(500, /Invalid status code/, function (err) { - if (isIoJs) { - done(err ? null : new Error('expected error')) - } else { - done(err) - } - }) - }) - }) + app.use(function (req, res) { + res.status(200).end(); + }); - describe('when "code" is null', function () { - it('should raise error for invalid status code', function (done) { + request(app) + .get('/') + .expect(200, done); + }); + + describe('accept valid ranges', function() { + // not testing w/ 100, because that has specific meaning and behavior in Node as Expect: 100-continue + it('should set the response status code to 101', function (done) { var app = express() app.use(function (req, res) { - res.status(null).end() + res.status(101).end() }) request(app) .get('/') - .expect(500, /Invalid status code/, function (err) { - if (isIoJs) { - done(err ? null : new Error('expected error')) - } else { - done(err) - } - }) + .expect(101, done) }) - }) - describe('when "code" is 201', function () { it('should set the response status code to 201', function (done) { var app = express() @@ -61,9 +42,7 @@ describe('res', function () { .get('/') .expect(201, done) }) - }) - describe('when "code" is 302', function () { it('should set the response status code to 302', function (done) { var app = express() @@ -75,9 +54,7 @@ describe('res', function () { .get('/') .expect(302, done) }) - }) - describe('when "code" is 403', function () { it('should set the response status code to 403', function (done) { var app = express() @@ -89,9 +66,7 @@ describe('res', function () { .get('/') .expect(403, done) }) - }) - describe('when "code" is 501', function () { it('should set the response status code to 501', function (done) { var app = express() @@ -103,100 +78,129 @@ describe('res', function () { .get('/') .expect(501, done) }) - }) - describe('when "code" is "410"', function () { - it('should set the response status code to 410', function (done) { + it('should set the response status code to 700', function (done) { var app = express() app.use(function (req, res) { - res.status('410').end() + res.status(700).end() }) request(app) .get('/') - .expect(410, done) + .expect(700, done) }) - }) - describe('when "code" is 410.1', function () { - it('should set the response status code to 410', function (done) { + it('should set the response status code to 800', function (done) { var app = express() app.use(function (req, res) { - res.status(410.1).end() + res.status(800).end() }) request(app) .get('/') - .expect(410, function (err) { - if (isIoJs) { - done(err ? null : new Error('expected error')) - } else { - done(err) - } - }) + .expect(800, done) }) - }) - describe('when "code" is 1000', function () { - it('should raise error for invalid status code', function (done) { + it('should set the response status code to 900', function (done) { var app = express() app.use(function (req, res) { - res.status(1000).end() + res.status(900).end() }) request(app) .get('/') - .expect(500, /Invalid status code/, function (err) { - if (isIoJs) { - done(err ? null : new Error('expected error')) - } else { - done(err) - } - }) + .expect(900, done) }) }) - describe('when "code" is 99', function () { - it('should raise error for invalid status code', function (done) { - var app = express() + describe('invalid status codes', function () { + it('should raise error for status code below 100', function (done) { + var app = express(); app.use(function (req, res) { - res.status(99).end() - }) + res.status(99).end(); + }); request(app) .get('/') - .expect(500, /Invalid status code/, function (err) { - if (isIoJs) { - done(err ? null : new Error('expected error')) - } else { - done(err) - } - }) - }) - }) + .expect(500, /Invalid status code/, done); + }); - describe('when "code" is -401', function () { - it('should raise error for invalid status code', function (done) { - var app = express() + it('should raise error for status code above 999', function (done) { + var app = express(); app.use(function (req, res) { - res.status(-401).end() - }) + res.status(1000).end(); + }); request(app) .get('/') - .expect(500, /Invalid status code/, function (err) { - if (isIoJs) { - done(err ? null : new Error('expected error')) - } else { - done(err) - } - }) - }) - }) - }) -}) + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for non-integer status codes', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(200.1).end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for undefined status code', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(undefined).end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for null status code', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(null).end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for string status code', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status("200").end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + + it('should raise error for NaN status code', function (done) { + var app = express(); + + app.use(function (req, res) { + res.status(NaN).end(); + }); + + request(app) + .get('/') + .expect(500, /Invalid status code/, done); + }); + }); + }); +}); + diff --git a/test/res.type.js b/test/res.type.js index 980717a6e30..09285af3914 100644 --- a/test/res.type.js +++ b/test/res.type.js @@ -14,7 +14,7 @@ describe('res', function(){ request(app) .get('/') - .expect('Content-Type', 'application/javascript; charset=utf-8') + .expect('Content-Type', 'text/javascript; charset=utf-8') .end(done) }) diff --git a/test/res.vary.js b/test/res.vary.js index 1efc20b4453..ff3c9716529 100644 --- a/test/res.vary.js +++ b/test/res.vary.js @@ -6,7 +6,7 @@ var utils = require('./support/utils'); describe('res.vary()', function(){ describe('with no arguments', function(){ - it('should not set Vary', function (done) { + it('should throw error', function (done) { var app = express(); app.use(function (req, res) { @@ -16,8 +16,7 @@ describe('res.vary()', function(){ request(app) .get('/') - .expect(utils.shouldNotHaveHeader('Vary')) - .expect(200, done); + .expect(500, /field.*required/, done) }) }) diff --git a/test/support/tmpl.js b/test/support/tmpl.js index bab65669d33..e24b6fe773b 100644 --- a/test/support/tmpl.js +++ b/test/support/tmpl.js @@ -1,4 +1,4 @@ -var fs = require('fs'); +var fs = require('node:fs'); var variableRegExp = /\$([0-9a-zA-Z\.]+)/g; diff --git a/test/support/utils.js b/test/support/utils.js index 5b4062e0b2a..25022528ef6 100644 --- a/test/support/utils.js +++ b/test/support/utils.js @@ -4,8 +4,7 @@ * @private */ -var assert = require('assert'); -var Buffer = require('safe-buffer').Buffer +var assert = require('node:assert'); /** * Module exports. @@ -16,6 +15,7 @@ exports.shouldHaveBody = shouldHaveBody exports.shouldHaveHeader = shouldHaveHeader exports.shouldNotHaveBody = shouldNotHaveBody exports.shouldNotHaveHeader = shouldNotHaveHeader; +exports.shouldSkipQuery = shouldSkipQuery /** * Assert that a supertest response has a specific body. @@ -70,3 +70,16 @@ function shouldNotHaveHeader(header) { assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header); }; } + +function getMajorVersion(versionString) { + return versionString.split('.')[0]; +} + +function shouldSkipQuery(versionString) { + // Skipping HTTP QUERY tests on Node 21, it is reported in http.METHODS on 21.7.2 but not supported + // update this implementation to run on supported versions of 21 once they exist + // upstream tracking https://github.com/nodejs/node/issues/51562 + // express tracking issue: https://github.com/expressjs/express/issues/5615 + return Number(getMajorVersion(versionString)) === 21 +} + diff --git a/test/utils.js b/test/utils.js index 9a38ede6569..d1142266ac4 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,7 +1,6 @@ 'use strict' -var assert = require('assert'); -var Buffer = require('safe-buffer').Buffer +var assert = require('node:assert'); var utils = require('../lib/utils'); describe('utils.etag(body, encoding)', function(){ @@ -69,35 +68,3 @@ describe('utils.wetag(body, encoding)', function(){ 'W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"') }) }) - -describe('utils.isAbsolute()', function(){ - it('should support windows', function(){ - assert(utils.isAbsolute('c:\\')); - assert(utils.isAbsolute('c:/')); - assert(!utils.isAbsolute(':\\')); - }) - - it('should support windows unc', function(){ - assert(utils.isAbsolute('\\\\foo\\bar')) - }) - - it('should support unices', function(){ - assert(utils.isAbsolute('/foo/bar')); - assert(!utils.isAbsolute('foo/bar')); - }) -}) - -describe('utils.flatten(arr)', function(){ - it('should flatten an array', function(){ - var arr = ['one', ['two', ['three', 'four'], 'five']]; - var flat = utils.flatten(arr) - - assert.strictEqual(flat.length, 5) - assert.strictEqual(flat[0], 'one') - assert.strictEqual(flat[1], 'two') - assert.strictEqual(flat[2], 'three') - assert.strictEqual(flat[3], 'four') - assert.strictEqual(flat[4], 'five') - assert.ok(flat.every(function (v) { return typeof v === 'string' })) - }) -})