Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: expressjs/express
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: d854c43ea177d1faeea56189249fff8c24a764bd
Choose a base ref
...
head repository: expressjs/express
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: b28db2c12c3bd9cd763316824446f79bf81e0686
Choose a head ref

Commits on May 20, 2022

  1. build: mocha@10.0.0

    dougwilson committed May 20, 2022
    Copy the full SHA
    a2dfc56 View commit details
  2. build: ejs@3.1.8

    dougwilson committed May 20, 2022
    Copy the full SHA
    745a63f View commit details
  3. build: Node.js@18.1

    dougwilson committed May 20, 2022
    Copy the full SHA
    ab2c70b View commit details
  4. Copy the full SHA
    7ec5dd2 View commit details
  5. Copy the full SHA
    97f0a51 View commit details
  6. examples: remove unused function arguments in params

    closes #4914
    alxdrg authored and dougwilson committed May 20, 2022
    Copy the full SHA
    2c47827 View commit details

Commits on Aug 19, 2022

  1. build: Node.js@16.17

    dougwilson committed Aug 19, 2022
    Copy the full SHA
    8d98e86 View commit details
  2. build: Node.js@18.7

    dougwilson committed Aug 19, 2022
    Copy the full SHA
    97131bc View commit details
  3. build: Node.js@14.20

    dougwilson committed Aug 19, 2022
    Copy the full SHA
    ecd7572 View commit details
  4. build: supertest@6.2.4

    dougwilson committed Aug 19, 2022
    Copy the full SHA
    644f646 View commit details

Commits on Aug 20, 2022

  1. docs: use Node.js name style

    closes #4926
    REALSTEVEIG authored and dougwilson committed Aug 20, 2022
    Copy the full SHA
    33e8dc3 View commit details

Commits on Oct 6, 2022

  1. build: eslint@8.24.0

    dougwilson committed Oct 6, 2022
    Copy the full SHA
    340be0f View commit details
  2. deps: body-parser@1.20.1

    dougwilson committed Oct 6, 2022
    Copy the full SHA
    689d175 View commit details
  3. deps: qs@6.11.0

    dougwilson committed Oct 6, 2022
    Copy the full SHA
    24b3dc5 View commit details
  4. build: supertest@6.3.0

    dougwilson committed Oct 6, 2022
    Copy the full SHA
    f56ce73 View commit details

Commits on Oct 7, 2022

  1. build: Node.js@18.10

    closes #5014
    dougwilson committed Oct 7, 2022
    Copy the full SHA
    bb7907b View commit details

Commits on Oct 8, 2022

  1. docs: replace Freenode with Libera Chat

    closes #5013
    theabhinavdas authored and dougwilson committed Oct 8, 2022
    Copy the full SHA
    61f4049 View commit details
  2. 4.18.2

    dougwilson committed Oct 8, 2022
    Copy the full SHA
    8368dc1 View commit details

Commits on Nov 2, 2022

  1. docs: update git clone to https protocol

    closes #5032
    vcsjones authored and dougwilson committed Nov 2, 2022
    1
    Copy the full SHA
    06b2b14 View commit details
  2. build: Node.js@16.18

    abenhamdine authored and dougwilson committed Nov 2, 2022
    Copy the full SHA
    29e117e View commit details
  3. build: Node.js@18.12

    abenhamdine authored and dougwilson committed Nov 2, 2022
    Copy the full SHA
    723b677 View commit details
  4. build: actions/checkout@v3

    closes #5027
    armujahid authored and dougwilson committed Nov 2, 2022
    Copy the full SHA
    442fd46 View commit details

Commits on Feb 21, 2023

  1. lint: remove unused function arguments in tests

    closes #5124
    raksbisht authored and dougwilson committed Feb 21, 2023
    Copy the full SHA
    c6ee8d6 View commit details

Commits on Feb 22, 2023

  1. Copy the full SHA
    a1efd9d View commit details
  2. docs: fix typos in JSDoc comments

    closes #5117
    raksbisht authored and dougwilson committed Feb 22, 2023
    Copy the full SHA
    6b4c4f5 View commit details
  3. lint: remove unused parameters in examples

    closes #5113
    raksbisht authored and dougwilson committed Feb 22, 2023
    Copy the full SHA
    3c1d605 View commit details
  4. build: Node.js@16.19

    dougwilson committed Feb 22, 2023
    Copy the full SHA
    f05b5d0 View commit details
  5. build: Node.js@18.14

    dougwilson committed Feb 22, 2023
    Copy the full SHA
    546969d View commit details
  6. Copy the full SHA
    b9f7a97 View commit details

Commits on Feb 23, 2023

  1. Copy the full SHA
    506fbd6 View commit details
  2. deps: body-parser@1.20.2

    dougwilson committed Feb 23, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    1e42a98 View commit details
  3. build: mocha@10.2.0

    dougwilson committed Feb 23, 2023

    Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    60b7c67 View commit details
  4. build: eslint@8.34.0

    dougwilson committed Feb 23, 2023

    Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    8a76f39 View commit details
  5. docs: fix typos in history

    closes #5131
    raksbisht authored and dougwilson committed Feb 23, 2023

    Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    5ad9541 View commit details
  6. build: support Node.js 19.x

    abenhamdine authored and dougwilson committed Feb 23, 2023

    Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    9bc1742 View commit details
  7. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    74beeac View commit details

Commits on Feb 26, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0debedf View commit details

Commits on Mar 14, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    8c24fa8 View commit details
  2. build: ejs@3.1.9

    dougwilson committed Mar 14, 2023
    Copy the full SHA
    f4e48bc View commit details
  3. build: eslint@8.36.0

    dougwilson committed Mar 14, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    b8b2eff View commit details
  4. build: Node.js@18.15

    dougwilson committed Mar 14, 2023
    Copy the full SHA
    f540c3b View commit details

Commits on Apr 7, 2023

  1. Copy the full SHA
    91b6fb8 View commit details
  2. build: Node.js@16.20

    dougwilson committed Apr 7, 2023
    1

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    24e4a25 View commit details

Commits on May 16, 2023

  1. Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    3531987 View commit details

Commits on Jun 4, 2023

  1. tests: use random port in listen test

    closes #5162
    rluvaton authored and dougwilson committed Jun 4, 2023
    1

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    2a00da2 View commit details

Commits on Aug 23, 2023

  1. build: eslint@8.47.0

    dougwilson committed Aug 23, 2023

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    13df1de View commit details
  2. build: Node.js@18.17

    dougwilson committed Aug 23, 2023

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    8d8bfaa View commit details

Commits on Aug 24, 2023

  1. build: Node.js@19.9

    dougwilson committed Aug 24, 2023

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    02d1c39 View commit details

Commits on Nov 2, 2023

  1. build: actions/checkout@v4

    dougwilson committed Nov 2, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    a229207 View commit details

Commits on Feb 19, 2024

  1. docs: update TC governance rules

    closes #5483
    wesleytodd authored and dougwilson committed Feb 19, 2024

    Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    c4fe7de View commit details
77 changes: 59 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -31,6 +31,9 @@ jobs:
- 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
@@ -71,11 +74,11 @@ jobs:

- name: Node.js 8.x
node-version: "8.17"
npm-i: mocha@7.2.0
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
npm-i: mocha@7.2.0 nyc@14.1.1

- name: Node.js 10.x
node-version: "10.24"
@@ -87,27 +90,38 @@ jobs:

- 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.19"
node-version: "14.20"

- name: Node.js 15.x
node-version: "15.14"

- name: Node.js 16.x
node-version: "16.15"
node-version: "16.20"

- name: Node.js 17.x
node-version: "17.9"

- name: Node.js 18.x
node-version: "18.0"
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"

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Install Node.js ${{ matrix.node-version }}
shell: bash -eo pipefail -l {0}
@@ -118,7 +132,11 @@ jobs:
- name: Configure npm
run: |
npm config set loglevel error
npm config set shrinkwrap false
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 }}
@@ -131,8 +149,8 @@ jobs:
shell: bash
run: |
# eslint for linting
# - remove on Node.js < 10
if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then
# - 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 | \
@@ -149,29 +167,52 @@ jobs:
echo "node@$(node -v)"
echo "npm@$(npm -v)"
npm -s ls ||:
(npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }'
(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
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
uses: coverallsapp/github-action@master
run: |
mv ./coverage "./${{ matrix.name }}"
mkdir ./coverage
mv "./${{ matrix.name }}" "./coverage/${{ matrix.name }}"
- name: Upload code coverage
uses: actions/upload-artifact@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
flag-name: run-${{ matrix.test_number }}
parallel: true
name: coverage
path: ./coverage
retention-days: 1

coverage:
needs: test
runs-on: ubuntu-latest
steps:
- name: Upload code coverage
- 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 }}
parallel-finished: true
github-token: ${{ secrets.GITHUB_TOKEN }}
75 changes: 69 additions & 6 deletions Contributing.md
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ contributors can be involved in decision making.

* A **Contributor** is any individual creating or commenting on an issue or pull request.
* A **Committer** is a subset of contributors who have been given write access to the repository.
* A **Project Captain** is the lead maintainer of a repository.
* A **TC (Technical Committee)** is a group of committers representing the required technical
expertise to resolve rare disputes.
* A **Triager** is a subset of contributors who have been given triage access to the repository.
@@ -102,12 +103,74 @@ If a consensus cannot be reached that has no objections then a majority wins vot
is called. It is also expected that the majority of decisions made by the TC are via
a consensus seeking process and that voting is only used as a last-resort.

Resolution may involve returning the issue to committers with suggestions on how to
move forward towards a consensus. It is not expected that a meeting of the TC
Resolution may involve returning the issue to project captains with suggestions on
how to move forward towards a consensus. It is not expected that a meeting of the TC
will resolve all issues on its agenda during that meeting and may prefer to continue
the discussion happening among the committers.
the discussion happening among the project captains.

Members can be added to the TC at any time. Any committer can nominate another committer
Members can be added to the TC at any time. Any TC member can nominate another committer
to the TC and the TC uses its standard consensus seeking process to evaluate whether or
not to add this new member. Members who do not participate consistently at the level of
a majority of the other members are expected to resign.
not to add this new member. The TC will consist of a minimum of 3 active members and a
maximum of 10. If the TC should drop below 5 members the active TC members should nominate
someone new. If a TC member is stepping down, they are encouraged (but not required) to
nominate someone to take their place.

TC members will be added as admin's on the Github orgs, npm orgs, and other resources as
necessary to be effective in the role.

To remain "active" a TC member should have participation within the last 12 months and miss
no more than six consecutive TC meetings. Our goal is to increase participation, not punish
people for any lack of participation, this guideline should be only be used as such
(replace an inactive member with a new active one, for example). Members who do not meet this
are expected to step down. If A TC member does not step down, an issue can be opened in the
discussions repo to move them to inactive status. TC members who step down or are removed due
to inactivity will be moved into inactive status.

Inactive status members can become active members by self nomination if the TC is not already
larger than the maximum of 10. They will also be given preference if, while at max size, an
active member steps down.

## Project Captains

The Express TC can designate captains for individual projects/repos in the
organizations. These captains are responsible for being the primary
day-to-day maintainers of the repo on a technical and community front.
Repo captains are empowered with repo ownership and package publication rights.
When there are conflicts, especially on topics that effect the Express project
at large, captains are responsible to raise it up to the TC and drive
those conflicts to resolution. Captains are also responsible for making sure
community members follow the community guidelines, maintaining the repo
and the published package, as well as in providing user support.

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

46 changes: 41 additions & 5 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
4.19.2 / 2024-03-25
==========

* Improved fix for open redirect allow list bypass

4.19.1 / 2024-03-20
==========

* Allow passing non-strings to res.location with new encoding handling checks

4.19.0 / 2024-03-20
==========

* Prevent open redirect allow list bypass due to encodeurl
* deps: cookie@0.6.0

4.18.3 / 2024-02-29
==========

* Fix routing requests without method
* deps: body-parser@1.20.2
- Fix strict json error message on Node.js 19+
- deps: content-type@~1.0.5
- deps: raw-body@2.5.2
* deps: cookie@0.6.0
- Add `partitioned` option

4.18.2 / 2022-10-08
===================

* Fix regression routing a large stack in a single route
* deps: body-parser@1.20.1
- deps: qs@6.11.0
- perf: remove unnecessary object clone
* deps: qs@6.11.0

4.18.1 / 2022-04-29
===================

@@ -2102,7 +2138,7 @@
* deps: connect@2.21.0
- deprecate `connect(middleware)` -- use `app.use(middleware)` instead
- deprecate `connect.createServer()` -- use `connect()` instead
- fix `res.setHeader()` patch to work with with get -> append -> set pattern
- fix `res.setHeader()` patch to work with get -> append -> set pattern
- deps: compression@~1.0.8
- deps: errorhandler@~1.1.1
- deps: express-session@~1.5.0
@@ -3313,8 +3349,8 @@ Shaw]
* Added node v0.1.97 compatibility
* Added support for deleting cookies via Request#cookie('key', null)
* Updated haml submodule
* Fixed not-found page, now using using charset utf-8
* Fixed show-exceptions page, now using using charset utf-8
* Fixed not-found page, now using charset utf-8
* Fixed show-exceptions page, now using charset utf-8
* Fixed view support due to fs.readFile Buffers
* Changed; mime.type() no longer accepts ".type" due to node extname() changes

@@ -3349,7 +3385,7 @@ Shaw]
==================

* Added charset support via Request#charset (automatically assigned to 'UTF-8' when respond()'s
encoding is set to 'utf8' or 'utf-8'.
encoding is set to 'utf8' or 'utf-8').
* Added "encoding" option to Request#render(). Closes #299
* Added "dump exceptions" setting, which is enabled by default.
* Added simple ejs template engine support
@@ -3388,7 +3424,7 @@ Shaw]
* Added [haml.js](http://github.com/visionmedia/haml.js) submodule; removed haml-js
* Added callback function support to Request#halt() as 3rd/4th arg
* Added preprocessing of route param wildcards using param(). Closes #251
* Added view partial support (with collections etc)
* Added view partial support (with collections etc.)
* Fixed bug preventing falsey params (such as ?page=0). Closes #286
* Fixed setting of multiple cookies. Closes #199
* Changed; view naming convention is now NAME.TYPE.ENGINE (for example page.html.haml)
6 changes: 3 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[![Express Logo](https://i.cloudup.com/zfY6lL7eFa-3000x3000.png)](http://expressjs.com/)

Fast, unopinionated, minimalist web framework for [node](http://nodejs.org).
Fast, unopinionated, minimalist web framework for [Node.js](http://nodejs.org).

[![NPM Version][npm-version-image]][npm-url]
[![NPM Install Size][npm-install-size-image]][npm-install-size-url]
@@ -51,7 +51,7 @@ for more information.
## Docs & Community

* [Website and Documentation](http://expressjs.com/) - [[website repo](https://github.com/expressjs/expressjs.com)]
* [#express](https://webchat.freenode.net/?channels=express) on freenode IRC
* [#express](https://web.libera.chat/#express) on [Libera Chat](https://libera.chat) IRC
* [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
@@ -104,7 +104,7 @@ $ npm start
To view the examples, clone the Express repo and install the dependencies:

```console
$ git clone git://github.com/expressjs/express.git --depth 1
$ git clone https://github.com/expressjs/express.git --depth 1
$ cd express
$ npm install
```
6 changes: 6 additions & 0 deletions Release-Process.md
Original file line number Diff line number Diff line change
@@ -184,3 +184,9 @@ $ npm publish

**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,
22 changes: 16 additions & 6 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -15,11 +15,14 @@ environment:
- nodejs_version: "11.15"
- nodejs_version: "12.22"
- nodejs_version: "13.14"
- nodejs_version: "14.19"
- nodejs_version: "14.20"
- nodejs_version: "15.14"
- nodejs_version: "16.15"
- nodejs_version: "16.20"
- nodejs_version: "17.9"
- nodejs_version: "18.0"
- nodejs_version: "18.19"
- nodejs_version: "19.9"
- nodejs_version: "20.11"
- nodejs_version: "21.6"
cache:
- node_modules
install:
@@ -30,7 +33,11 @@ install:
# Configure npm
- ps: |
npm config set loglevel error
npm config set shrinkwrap false
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
@@ -47,6 +54,7 @@ install:
# - 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) {
@@ -57,17 +65,19 @@ install:
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 < 8
# - 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 8) {
} elseif ([int]$env:nodejs_version.split(".")[0] -lt 10) {
npm install --silent --save-dev nyc@14.1.1
}
- ps: |
34 changes: 34 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Express Benchmarks

## Installation

You will need to install [wrk](https://github.com/wg/wrk/blob/master/INSTALL) in order to run the benchmarks.

## Running

To run the benchmarks, first install the dependencies `npm i`, then run `make`

The output will look something like this:

```
50 connections
1 middleware
7.15ms
6784.01
[...redacted...]
1000 connections
10 middleware
139.21ms
6155.19
```

### Tip: Include Node.js version in output

You can use `make && node -v` to include the node.js version in the output.

### Tip: Save the results to a file

You can use `make > results.log` to save the results to a file `results.log`.
1 change: 0 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
@@ -13,7 +13,6 @@ This page contains list of examples using Express.
- [hello-world](./hello-world) - Simple request handler
- [markdown](./markdown) - Markdown as template engine
- [multi-router](./multi-router) - Working with multiple Express routers
- [multipart](./multipart) - Accepting multipart-encoded forms
- [mvc](./mvc) - MVC-style controllers
- [online](./online) - Tracking online user activity with `online` and `redis` packages
- [params](./params) - Working with route parameters
7 changes: 2 additions & 5 deletions examples/cookie-sessions/index.js
Original file line number Diff line number Diff line change
@@ -13,13 +13,10 @@ var app = module.exports = express();
app.use(cookieSession({ secret: 'manny is cool' }));

// do something with the session
app.use(count);

// custom middleware
function count(req, res) {
app.get('/', function (req, res) {
req.session.count = (req.session.count || 0) + 1
res.send('viewed ' + req.session.count + ' times\n')
}
})

/* istanbul ignore next */
if (!module.parent) {
2 changes: 1 addition & 1 deletion examples/error/index.js
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ function error(err, req, res, next) {
res.send('Internal Server Error');
}

app.get('/', function(req, res){
app.get('/', function () {
// Caught and passed down to the errorHandler middleware
throw new Error('something broke!');
});
2 changes: 1 addition & 1 deletion examples/markdown/index.js
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ app.engine('md', function(path, options, fn){

app.set('views', path.join(__dirname, 'views'));

// make it the default so we dont need .md
// make it the default, so we don't need .md
app.set('view engine', 'md');

app.get('/', function(req, res){
62 changes: 0 additions & 62 deletions examples/multipart/index.js

This file was deleted.

4 changes: 2 additions & 2 deletions examples/params/index.js
Original file line number Diff line number Diff line change
@@ -51,15 +51,15 @@ app.get('/', function(req, res){
* GET :user.
*/

app.get('/user/:user', function(req, res, next){
app.get('/user/:user', function (req, res) {
res.send('user ' + req.user.name);
});

/**
* GET users :from - :to.
*/

app.get('/users/:from-:to', function(req, res, next){
app.get('/users/:from-:to', function (req, res) {
var from = req.params.from;
var to = req.params.to;
var names = users.map(function(user){ return user.name; });
4 changes: 2 additions & 2 deletions examples/view-locals/index.js
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ function users(req, res, next) {
})
}

app.get('/middleware', count, users, function(req, res, next){
app.get('/middleware', count, users, function (req, res) {
res.render('index', {
title: 'Users',
count: req.count,
@@ -99,7 +99,7 @@ function users2(req, res, next) {
})
}

app.get('/middleware-locals', count2, users2, function(req, res, next){
app.get('/middleware-locals', count2, users2, function (req, res) {
// you can see now how we have much less
// to pass to res.render(). If we have
// several routes related to users this
4 changes: 2 additions & 2 deletions examples/web-service/index.js
Original file line number Diff line number Diff line change
@@ -72,12 +72,12 @@ var userRepos = {
// and simply expose the data

// example: http://localhost:3000/api/users/?api-key=foo
app.get('/api/users', function(req, res, next){
app.get('/api/users', function (req, res) {
res.send(users);
});

// example: http://localhost:3000/api/repos/?api-key=foo
app.get('/api/repos', function(req, res, next){
app.get('/api/repos', function (req, res) {
res.send(repos);
});

15 changes: 12 additions & 3 deletions lib/response.js
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@ module.exports = res
*/

var charsetRegExp = /;\s*charset\s*=/;
var schemaAndHostRegExp = /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?\/\/[^\\\/\?]+/;

/**
* Set status `code`.
@@ -904,15 +905,23 @@ res.cookie = function (name, value, options) {
*/

res.location = function location(url) {
var loc = url;
var loc;

// "back" is an alias for the referrer
if (url === 'back') {
loc = this.req.get('Referrer') || '/';
} else {
loc = String(url);
}

// set location
return this.set('Location', encodeUrl(loc));
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);
};

/**
2 changes: 1 addition & 1 deletion lib/router/index.js
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ var toString = Object.prototype.toString;
* Initialize a new `Router` with the given `options`.
*
* @param {Object} [options]
* @return {Router} which is an callable function
* @return {Router} which is a callable function
* @public
*/

25 changes: 15 additions & 10 deletions lib/router/route.js
Original file line number Diff line number Diff line change
@@ -60,7 +60,10 @@ Route.prototype._handles_method = function _handles_method(method) {
return true;
}

var name = method.toLowerCase();
// normalize name
var name = typeof method === 'string'
? method.toLowerCase()
: method

if (name === 'head' && !this.methods['head']) {
name = 'get';
@@ -103,8 +106,10 @@ Route.prototype.dispatch = function dispatch(req, res, done) {
if (stack.length === 0) {
return done();
}
var method = typeof req.method === 'string'
? req.method.toLowerCase()
: req.method

var method = req.method.toLowerCase();
if (method === 'head' && !this.methods['head']) {
method = 'get';
}
@@ -124,21 +129,21 @@ Route.prototype.dispatch = function dispatch(req, res, done) {
return done(err)
}

var layer = stack[idx++];
if (!layer) {
return done(err);
}

// max sync stack
if (++sync > 100) {
return setImmediate(next, err)
}

if (layer.method && layer.method !== method) {
return next(err);
var layer = stack[idx++]

// end of layers
if (!layer) {
return done(err)
}

if (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);
7 changes: 3 additions & 4 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -117,17 +117,15 @@ exports.contentDisposition = deprecate.function(contentDisposition,
/**
* Parse accept params `str` returning an
* object with `.value`, `.quality` and `.params`.
* also includes `.originalIndex` for stable sorting
*
* @param {String} str
* @param {Number} index
* @return {Object}
* @api private
*/

function acceptParams(str, index) {
function acceptParams (str) {
var parts = str.split(/ *; */);
var ret = { value: parts[0], quality: 1, params: {}, originalIndex: index };
var ret = { value: parts[0], quality: 1, params: {} }

for (var i = 1; i < parts.length; ++i) {
var pms = parts[i].split(/ *= */);
@@ -282,6 +280,7 @@ function createETagGenerator (options) {
/**
* Parse an extended query string with qs.
*
* @param {String} str
* @return {Object}
* @private
*/
17 changes: 8 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "express",
"description": "Fast, unopinionated, minimalist web framework",
"version": "4.18.1",
"version": "4.19.2",
"author": "TJ Holowaychuk <tj@vision-media.ca>",
"contributors": [
"Aaron Heckmann <aaron.heckmann+github@gmail.com>",
@@ -30,10 +30,10 @@
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.0",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -49,7 +49,7 @@
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.10.3",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
@@ -65,18 +65,17 @@
"connect-redis": "3.4.2",
"cookie-parser": "1.4.6",
"cookie-session": "2.0.0",
"ejs": "3.1.7",
"eslint": "7.32.0",
"ejs": "3.1.9",
"eslint": "8.47.0",
"express-session": "1.17.2",
"hbs": "4.2.0",
"marked": "0.7.0",
"method-override": "3.0.0",
"mocha": "9.2.2",
"mocha": "10.2.0",
"morgan": "1.10.0",
"multiparty": "4.2.3",
"nyc": "15.1.0",
"pbkdf2-password": "1.2.1",
"supertest": "6.2.3",
"supertest": "6.3.0",
"vhost": "~3.0.2"
},
"engines": {
17 changes: 13 additions & 4 deletions test/Route.js
Original file line number Diff line number Diff line change
@@ -19,8 +19,16 @@ describe('Route', function(){
var req = { method: 'GET', url: '/' }
var route = new Route('/foo')

route.get(function (req, res, next) {
req.counter = 0
next()
})

for (var i = 0; i < 6000; i++) {
route.all(function (req, res, next) { next() })
route.all(function (req, res, next) {
req.counter++
next()
})
}

route.get(function (req, res, next) {
@@ -31,6 +39,7 @@ describe('Route', function(){
route.dispatch(req, {}, function (err) {
if (err) return done(err)
assert.ok(req.called)
assert.strictEqual(req.counter, 6000)
done()
})
})
@@ -115,7 +124,7 @@ describe('Route', function(){
var req = { method: 'POST', url: '/' };
var route = new Route('');

route.get(function(req, res, next) {
route.get(function () {
throw new Error('not me!');
})

@@ -189,7 +198,7 @@ describe('Route', function(){
var req = { order: '', method: 'GET', url: '/' };
var route = new Route('');

route.all(function(req, res, next){
route.all(function () {
throw new Error('foobar');
});

@@ -215,7 +224,7 @@ describe('Route', function(){
var req = { method: 'GET', url: '/' };
var route = new Route('');

route.get(function(req, res, next){
route.get(function () {
throw new Error('boom!');
});

57 changes: 51 additions & 6 deletions test/Router.js
Original file line number Diff line number Diff line change
@@ -61,6 +61,33 @@ describe('Router', function(){
router.handle({ method: 'GET' }, {}, done)
})

it('handle missing method', function (done) {
var all = false
var router = new Router()
var route = router.route('/foo')
var use = false

route.post(function (req, res, next) { next(new Error('should not run')) })
route.all(function (req, res, next) {
all = true
next()
})
route.get(function (req, res, next) { next(new Error('should not run')) })

router.get('/foo', function (req, res, next) { next(new Error('should not run')) })
router.use(function (req, res, next) {
use = true
next()
})

router.handle({ url: '/foo' }, {}, function (err) {
if (err) return done(err)
assert.ok(all)
assert.ok(use)
done()
})
})

it('should not stack overflow with many registered routes', function(done){
this.timeout(5000) // long-running test

@@ -83,11 +110,20 @@ describe('Router', function(){

var router = new Router()

router.get('/foo', function (req, res, next) {
req.counter = 0
next()
})

for (var i = 0; i < 6000; i++) {
router.get('/foo', function (req, res, next) { next() })
router.get('/foo', function (req, res, next) {
req.counter++
next()
})
}

router.get('/foo', function (req, res) {
assert.strictEqual(req.counter, 6000)
res.end()
})

@@ -99,11 +135,20 @@ describe('Router', function(){

var router = new Router()

router.use(function (req, res, next) {
req.counter = 0
next()
})

for (var i = 0; i < 6000; i++) {
router.use(function (req, res, next) { next() })
router.use(function (req, res, next) {
req.counter++
next()
})
}

router.use(function (req, res) {
assert.strictEqual(req.counter, 6000)
res.end()
})

@@ -183,7 +228,7 @@ describe('Router', function(){
it('should handle throwing inside routes with params', function(done) {
var router = new Router();

router.get('/foo/:id', function(req, res, next){
router.get('/foo/:id', function () {
throw new Error('foo');
});

@@ -561,8 +606,8 @@ describe('Router', function(){
var req2 = { url: '/foo/10/bar', method: 'get' };
var router = new Router();
var sub = new Router();
var cb = after(2, done)

done = after(2, done);

sub.get('/bar', function(req, res, next) {
next();
@@ -581,14 +626,14 @@ describe('Router', function(){
assert.ifError(err);
assert.equal(req1.ms, 50);
assert.equal(req1.originalUrl, '/foo/50/bar');
done();
cb()
});

router.handle(req2, {}, function(err) {
assert.ifError(err);
assert.equal(req2.ms, 10);
assert.equal(req2.originalUrl, '/foo/10/bar');
done();
cb()
});
});
});
5 changes: 2 additions & 3 deletions test/app.listen.js
Original file line number Diff line number Diff line change
@@ -6,9 +6,8 @@ describe('app.listen()', function(){
it('should wrap with an HTTP server', function(done){
var app = express();

var server = app.listen(9999, function(){
server.close();
done();
var server = app.listen(0, function () {
server.close(done)
});
})
})
6 changes: 3 additions & 3 deletions test/app.param.js
Original file line number Diff line number Diff line change
@@ -166,7 +166,7 @@ describe('app', function(){
app.get('/:user', function(req, res, next) {
next('route');
});
app.get('/:user', function(req, res, next) {
app.get('/:user', function (req, res) {
res.send(req.params.user);
});

@@ -187,11 +187,11 @@ describe('app', function(){
next(new Error('invalid invocation'))
});

app.post('/:user', function(req, res, next) {
app.post('/:user', function (req, res) {
res.send(req.params.user);
});

app.get('/:thing', function(req, res, next) {
app.get('/:thing', function (req, res) {
res.send(req.thing);
});

24 changes: 12 additions & 12 deletions test/app.router.js
Original file line number Diff line number Diff line change
@@ -90,7 +90,7 @@ describe('app.router', function(){
it('should decode correct params', function(done){
var app = express();

app.get('/:name', function(req, res, next){
app.get('/:name', function (req, res) {
res.send(req.params.name);
});

@@ -102,7 +102,7 @@ describe('app.router', function(){
it('should not accept params in malformed paths', function(done) {
var app = express();

app.get('/:name', function(req, res, next){
app.get('/:name', function (req, res) {
res.send(req.params.name);
});

@@ -114,7 +114,7 @@ describe('app.router', function(){
it('should not decode spaces', function(done) {
var app = express();

app.get('/:name', function(req, res, next){
app.get('/:name', function (req, res) {
res.send(req.params.name);
});

@@ -126,7 +126,7 @@ describe('app.router', function(){
it('should work with unicode', function(done) {
var app = express();

app.get('/:name', function(req, res, next){
app.get('/:name', function (req, res) {
res.send(req.params.name);
});

@@ -896,7 +896,7 @@ describe('app.router', function(){

request(app)
.get('/foo.json')
.expect(200, 'foo as json', done)
.expect(200, 'foo as json', cb)
})
})

@@ -910,7 +910,7 @@ describe('app.router', function(){
next();
});

app.get('/bar', function(req, res){
app.get('/bar', function () {
assert(0);
});

@@ -919,7 +919,7 @@ describe('app.router', function(){
next();
});

app.get('/foo', function(req, res, next){
app.get('/foo', function (req, res) {
calls.push('/foo 2');
res.json(calls)
});
@@ -939,7 +939,7 @@ describe('app.router', function(){
next('route')
}

app.get('/foo', fn, function(req, res, next){
app.get('/foo', fn, function (req, res) {
res.end('failure')
});

@@ -964,11 +964,11 @@ describe('app.router', function(){
next('router')
}

router.get('/foo', fn, function (req, res, next) {
router.get('/foo', fn, function (req, res) {
res.end('failure')
})

router.get('/foo', function (req, res, next) {
router.get('/foo', function (req, res) {
res.end('failure')
})

@@ -995,7 +995,7 @@ describe('app.router', function(){
next();
});

app.get('/bar', function(req, res){
app.get('/bar', function () {
assert(0);
});

@@ -1004,7 +1004,7 @@ describe('app.router', function(){
next(new Error('fail'));
});

app.get('/foo', function(req, res, next){
app.get('/foo', function () {
assert(0);
});

2 changes: 1 addition & 1 deletion test/app.use.js
Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@ describe('app', function(){

request(app)
.get('/forum')
.expect(200, 'forum', done)
.expect(200, 'forum', cb)
})

it('should set the child\'s .parent', function(){
8 changes: 4 additions & 4 deletions test/express.json.js
Original file line number Diff line number Diff line change
@@ -262,7 +262,7 @@ describe('express.json()', function () {
.post('/')
.set('Content-Type', 'application/json')
.send('true')
.expect(400, '[entity.parse.failed] ' + parseError('#rue').replace('#', 't'), done)
.expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done)
})
})

@@ -290,15 +290,15 @@ describe('express.json()', function () {
.post('/')
.set('Content-Type', 'application/json')
.send('true')
.expect(400, '[entity.parse.failed] ' + parseError('#rue').replace('#', 't'), done)
.expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done)
})

it('should not parse primitives with leading whitespaces', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'application/json')
.send(' true')
.expect(400, '[entity.parse.failed] ' + parseError(' #rue').replace('#', 't'), done)
.expect(400, '[entity.parse.failed] ' + parseError(' #rue').replace(/#/g, 't'), done)
})

it('should allow leading whitespaces in JSON', function (done) {
@@ -316,7 +316,7 @@ describe('express.json()', function () {
.set('X-Error-Property', 'stack')
.send('true')
.expect(400)
.expect(shouldContainInBody(parseError('#rue').replace('#', 't')))
.expect(shouldContainInBody(parseError('#rue').replace(/#/g, 't')))
.end(done)
})
})
16 changes: 16 additions & 0 deletions test/res.cookie.js
Original file line number Diff line number Diff line change
@@ -82,6 +82,22 @@ describe('res', function(){
})
})

describe('partitioned', function () {
it('should set partitioned', function (done) {
var app = express();

app.use(function (req, res) {
res.cookie('name', 'tobi', { partitioned: true });
res.end();
});

request(app)
.get('/')
.expect('Set-Cookie', 'name=tobi; Path=/; Partitioned')
.expect(200, done)
})
})

describe('maxAge', function(){
it('should set relative expires', function(done){
var app = express();
4 changes: 2 additions & 2 deletions test/res.format.js
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ app3.use(function(req, res, next){

var app4 = express();

app4.get('/', function(req, res, next){
app4.get('/', function (req, res) {
res.format({
text: function(){ res.send('hey') },
html: function(){ res.send('<p>hey</p>') },
@@ -155,7 +155,7 @@ describe('res', function(){
var app = express();
var router = express.Router();

router.get('/', function(req, res, next){
router.get('/', function (req, res) {
res.format({
text: function(){ res.send('hey') },
html: function(){ res.send('<p>hey</p>') },
304 changes: 292 additions & 12 deletions test/res.location.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,48 @@
'use strict'

var express = require('../')
, request = require('supertest');
, request = require('supertest')
, assert = require('assert')
, url = require('url');

describe('res', function(){
describe('.location(url)', function(){
it('should set the header', function(done){
var app = express();

app.use(function(req, res){
res.location('http://google.com').end();
res.location('http://google.com/').end();
});

request(app)
.get('/')
.expect('Location', 'http://google.com')
.expect('Location', 'http://google.com/')
.expect(200, done)
})

it('should encode "url"', function (done) {
var app = express()
it('should preserve trailing slashes when not present', function(done){
var app = express();

app.use(function (req, res) {
res.location('https://google.com?q=\u2603 §10').end()
})
app.use(function(req, res){
res.location('http://google.com').end();
});

request(app)
.get('/')
.expect('Location', 'https://google.com?q=%E2%98%83%20%C2%A710')
.expect('Location', 'http://google.com')
.expect(200, done)
})

it('should not touch already-encoded sequences in "url"', function (done) {
it('should encode "url"', function (done) {
var app = express()

app.use(function (req, res) {
res.location('https://google.com?q=%A710').end()
res.location('https://google.com?q=\u2603 §10').end()
})

request(app)
.get('/')
.expect('Location', 'https://google.com?q=%A710')
.expect('Location', 'https://google.com?q=%E2%98%83%20%C2%A710')
.expect(200, done)
})

@@ -101,5 +103,283 @@ describe('res', function(){
.expect(200, done)
})
})

it('should encode data uri', function (done) {
var app = express()
app.use(function (req, res) {
res.location('data:text/javascript,export default () => { }').end();
});

request(app)
.get('/')
.expect('Location', 'data:text/javascript,export%20default%20()%20=%3E%20%7B%20%7D')
.expect(200, done)
})

it('should encode data uri', function (done) {
var app = express()
app.use(function (req, res) {
res.location('data:text/javascript,export default () => { }').end();
});

request(app)
.get('/')
.expect('Location', 'data:text/javascript,export%20default%20()%20=%3E%20%7B%20%7D')
.expect(200, done)
})

it('should consistently handle non-string input: boolean', function (done) {
var app = express()
app.use(function (req, res) {
res.location(true).end();
});

request(app)
.get('/')
.expect('Location', 'true')
.expect(200, done)
});

it('should consistently handle non-string inputs: object', function (done) {
var app = express()
app.use(function (req, res) {
res.location({}).end();
});

request(app)
.get('/')
.expect('Location', '[object%20Object]')
.expect(200, done)
});

it('should consistently handle non-string inputs: array', function (done) {
var app = express()
app.use(function (req, res) {
res.location([]).end();
});

request(app)
.get('/')
.expect('Location', '')
.expect(200, done)
});

it('should consistently handle empty string input', function (done) {
var app = express()
app.use(function (req, res) {
res.location('').end();
});

request(app)
.get('/')
.expect('Location', '')
.expect(200, done)
});


if (typeof URL !== 'undefined') {
it('should accept an instance of URL', function (done) {
var app = express();

app.use(function(req, res){
res.location(new URL('http://google.com/')).end();
});

request(app)
.get('/')
.expect('Location', 'http://google.com/')
.expect(200, done);
});
}
})

describe('location header encoding', function() {
function createRedirectServerForDomain (domain) {
var app = express();
app.use(function (req, res) {
var host = url.parse(req.query.q, false, true).host;
// This is here to show a basic check one might do which
// would pass but then the location header would still be bad
if (host !== domain) {
res.status(400).end('Bad host: ' + host + ' !== ' + domain);
}
res.location(req.query.q).end();
});
return app;
}

function testRequestedRedirect (app, inputUrl, expected, expectedHost, done) {
return request(app)
// Encode uri because old supertest does not and is required
// to test older node versions. New supertest doesn't re-encode
// so this works in both.
.get('/?q=' + encodeURIComponent(inputUrl))
.expect('') // No body.
.expect(200)
.expect('Location', expected)
.end(function (err, res) {
if (err) {
console.log('headers:', res.headers)
console.error('error', res.error, err);
return done(err, res);
}

// Parse the hosts from the input URL and the Location header
var inputHost = url.parse(inputUrl, false, true).host;
var locationHost = url.parse(res.headers['location'], false, true).host;

assert.strictEqual(locationHost, expectedHost);

// Assert that the hosts are the same
if (inputHost !== locationHost) {
return done(new Error('Hosts do not match: ' + inputHost + " !== " + locationHost));
}

return done(null, res);
});
}

it('should not touch already-encoded sequences in "url"', function (done) {
var app = createRedirectServerForDomain('google.com');
testRequestedRedirect(
app,
'https://google.com?q=%A710',
'https://google.com?q=%A710',
'google.com',
done
);
});

it('should consistently handle relative urls', function (done) {
var app = createRedirectServerForDomain(null);
testRequestedRedirect(
app,
'/foo/bar',
'/foo/bar',
null,
done
);
});

it('should not encode urls in such a way that they can bypass redirect allow lists', function (done) {
var app = createRedirectServerForDomain('google.com');
testRequestedRedirect(
app,
'http://google.com\\@apple.com',
'http://google.com\\@apple.com',
'google.com',
done
);
});

it('should not be case sensitive', function (done) {
var app = createRedirectServerForDomain('google.com');
testRequestedRedirect(
app,
'HTTP://google.com\\@apple.com',
'HTTP://google.com\\@apple.com',
'google.com',
done
);
});

it('should work with https', function (done) {
var app = createRedirectServerForDomain('google.com');
testRequestedRedirect(
app,
'https://google.com\\@apple.com',
'https://google.com\\@apple.com',
'google.com',
done
);
});

it('should correctly encode schemaless paths', function (done) {
var app = createRedirectServerForDomain('google.com');
testRequestedRedirect(
app,
'//google.com\\@apple.com/',
'//google.com\\@apple.com/',
'google.com',
done
);
});

it('should percent encode 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',
'google.com',
done
);
});

it('should escape header splitting for old node versions', function (done) {
var app = createRedirectServerForDomain('google.com');
testRequestedRedirect(
app,
'http://google.com\\@apple.com/%0d%0afoo:%20bar',
'http://google.com\\@apple.com/%0d%0afoo:%20bar',
'google.com',
done
);
});

it('should encode unicode correctly', function (done) {
var app = createRedirectServerForDomain(null);
testRequestedRedirect(
app,
'/%e2%98%83',
'/%e2%98%83',
null,
done
);
});

it('should encode unicode correctly even with a bad host', function (done) {
var app = createRedirectServerForDomain('google.com');
testRequestedRedirect(
app,
'http://google.com\\@apple.com/%e2%98%83',
'http://google.com\\@apple.com/%e2%98%83',
'google.com',
done
);
});

it('should work correctly despite using deprecated url.parse', function (done) {
var app = createRedirectServerForDomain('google.com');
testRequestedRedirect(
app,
'https://google.com\'.bb.com/1.html',
'https://google.com\'.bb.com/1.html',
'google.com',
done
);
});

it('should encode file uri path', function (done) {
var app = createRedirectServerForDomain('');
testRequestedRedirect(
app,
'file:///etc\\passwd',
'file:///etc%5Cpasswd',
'',
done
);
});
});
})
7 changes: 4 additions & 3 deletions test/res.sendFile.js
Original file line number Diff line number Diff line change
@@ -1050,12 +1050,13 @@ describe('res', function(){

app.use(function(req, res){
res.sendfile('test/fixtures/user.html', function(err){
assert(!res.headersSent);
assert.strictEqual(req.socket.listeners('error').length, 1) // node's original handler
assert.ok(err)
assert.ok(!res.headersSent)
assert.strictEqual(err.message, 'broken!')
done();
});

req.socket.emit('error', new Error('broken!'));
req.socket.destroy(new Error('broken!'))
});

request(app)