Merge tag '4.19.0' into 5.x

4.19.0
This commit is contained in:
Wes Todd 2024-03-20 21:14:00 -05:00
commit e9f9aaeebd
53 changed files with 2821 additions and 772 deletions

View File

@ -23,6 +23,13 @@ jobs:
- Node.js 12.x - Node.js 12.x
- Node.js 13.x - Node.js 13.x
- Node.js 14.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: include:
- name: Node.js 4.0 - name: Node.js 4.0
@ -39,7 +46,7 @@ jobs:
- name: Node.js 6.x - name: Node.js 6.x
node-version: "6.17" node-version: "6.17"
npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 npm-i: mocha@6.2.2 nyc@14.1.1 supertest@3.4.2
- name: Node.js 7.x - name: Node.js 7.x
node-version: "7.10" node-version: "7.10"
@ -47,11 +54,11 @@ jobs:
- name: Node.js 8.x - name: Node.js 8.x
node-version: "8.17" 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 - name: Node.js 9.x
node-version: "9.11" 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 - name: Node.js 10.x
node-version: "10.24" node-version: "10.24"
@ -63,15 +70,38 @@ jobs:
- name: Node.js 12.x - name: Node.js 12.x
node-version: "12.22" node-version: "12.22"
npm-i: mocha@9.2.2
- name: Node.js 13.x - name: Node.js 13.x
node-version: "13.14" node-version: "13.14"
npm-i: mocha@9.2.2
- name: Node.js 14.x - 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.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"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Node.js ${{ matrix.node-version }} - name: Install Node.js ${{ matrix.node-version }}
shell: bash -eo pipefail -l {0} shell: bash -eo pipefail -l {0}
@ -82,7 +112,11 @@ jobs:
- name: Configure npm - name: Configure npm
run: | run: |
npm config set loglevel error 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 npm config set shrinkwrap false
fi
- name: Install npm module(s) ${{ matrix.npm-i }} - name: Install npm module(s) ${{ matrix.npm-i }}
run: npm install --save-dev ${{ matrix.npm-i }} run: npm install --save-dev ${{ matrix.npm-i }}
@ -95,8 +129,8 @@ jobs:
shell: bash shell: bash
run: | run: |
# eslint for linting # eslint for linting
# - remove on Node.js < 10 # - remove on Node.js < 12
if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then
node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \
grep -E '^eslint(-|$)' | \ grep -E '^eslint(-|$)' | \
sort -r | \ sort -r | \
@ -113,29 +147,52 @@ jobs:
echo "node@$(node -v)" echo "node@$(node -v)"
echo "npm@$(npm -v)" echo "npm@$(npm -v)"
npm -s ls ||: 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 - name: Run tests
shell: bash shell: bash
run: npm run test-ci run: |
npm run test-ci
cp coverage/lcov.info "coverage/${{ matrix.name }}.lcov"
- name: Lint code - name: Lint code
if: steps.list_env.outputs.eslint != '' if: steps.list_env.outputs.eslint != ''
run: npm run lint run: npm run lint
- name: Collect code coverage - 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: with:
github-token: ${{ secrets.GITHUB_TOKEN }} name: coverage
flag-name: run-${{ matrix.test_number }} path: ./coverage
parallel: true retention-days: 1
coverage: coverage:
needs: test needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 uses: coverallsapp/github-action@master
with: with:
github-token: ${{ secrets.github_token }} github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true

View File

@ -9,7 +9,7 @@ also easily visible to outsiders.
## Section 1: Scope ## Section 1: Scope
Express is a http web server framework with a simple and expressive API Express is a HTTP web server framework with a simple and expressive API
which is highly aligned with Node.js core. We aim to be the best in which is highly aligned with Node.js core. We aim to be the best in
class for writing performant, spec compliant, and powerful web servers class for writing performant, spec compliant, and powerful web servers
in Node.js. As one of the oldest and most popular web frameworks in in Node.js. As one of the oldest and most popular web frameworks in
@ -24,7 +24,7 @@ Express is made of many modules spread between three GitHub Orgs:
libraries libraries
- [pillarjs](http://github.com/pillarjs/): Components which make up - [pillarjs](http://github.com/pillarjs/): Components which make up
Express but can also be used for other web frameworks Express but can also be used for other web frameworks
- [jshttp](http://github.com/jshttp/): Low level http libraries - [jshttp](http://github.com/jshttp/): Low level HTTP libraries
### 1.2: Out-of-Scope ### 1.2: Out-of-Scope

View File

@ -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 **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 **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 * A **TC (Technical Committee)** is a group of committers representing the required technical
expertise to resolve rare disputes. expertise to resolve rare disputes.
* A **Triager** is a subset of contributors who have been given triage access to the repository. * 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 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. 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 Resolution may involve returning the issue to project captains with suggestions on
move forward towards a consensus. It is not expected that a meeting of the TC 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 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 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 not to add this new member. The TC will consist of a minimum of 3 active members and a
a majority of the other members are expected to resign. 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

View File

@ -162,6 +162,86 @@ This is the first Express 5.0 alpha release, based off 4.10.1.
* add: * add:
- `app.router` is a reference to the base router - `app.router` is a reference to the base router
4.18.3 / 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
===================
* Fix hanging on large stack of sync routes
4.18.0 / 2022-04-25
===================
* Add "root" option to `res.download`
* Allow `options` without `filename` in `res.download`
* Deprecate string and non-integer arguments to `res.status`
* Fix behavior of `null`/`undefined` as `maxAge` in `res.cookie`
* Fix handling very large stacks of sync middleware
* Ignore `Object.prototype` values in settings through `app.set`/`app.get`
* Invoke `default` with same arguments as types in `res.format`
* Support proper 205 responses using `res.send`
* Use `http-errors` for `res.format` error
* deps: body-parser@1.20.0
- Fix error message for json parse whitespace in `strict`
- Fix internal error when inflated body exceeds limit
- Prevent loss of async hooks context
- Prevent hanging when request already read
- deps: depd@2.0.0
- deps: http-errors@2.0.0
- deps: on-finished@2.4.1
- deps: qs@6.10.3
- deps: raw-body@2.5.1
* deps: cookie@0.5.0
- Add `priority` option
- Fix `expires` option to reject invalid dates
* deps: depd@2.0.0
- Replace internal `eval` usage with `Function` constructor
- Use instance methods on `process` to check for listeners
* deps: finalhandler@1.2.0
- Remove set content headers that break response
- deps: on-finished@2.4.1
- deps: statuses@2.0.1
* deps: on-finished@2.4.1
- Prevent loss of async hooks context
* deps: qs@6.10.3
* deps: send@0.18.0
- Fix emitted 416 error missing headers property
- Limit the headers removed for 304 response
- deps: depd@2.0.0
- deps: destroy@1.2.0
- deps: http-errors@2.0.0
- deps: on-finished@2.4.1
- deps: statuses@2.0.1
* deps: serve-static@1.15.0
- deps: send@0.18.0
* deps: statuses@2.0.1
- Remove code 306
- Rename `425 Unordered Collection` to standard `425 Too Early`
4.17.3 / 2022-02-16 4.17.3 / 2022-02-16
=================== ===================
@ -2212,7 +2292,7 @@ This is the first Express 5.0 alpha release, based off 4.10.1.
* deps: connect@2.21.0 * deps: connect@2.21.0
- deprecate `connect(middleware)` -- use `app.use(middleware)` instead - deprecate `connect(middleware)` -- use `app.use(middleware)` instead
- deprecate `connect.createServer()` -- use `connect()` 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: compression@~1.0.8
- deps: errorhandler@~1.1.1 - deps: errorhandler@~1.1.1
- deps: express-session@~1.5.0 - deps: express-session@~1.5.0
@ -3423,8 +3503,8 @@ Shaw]
* Added node v0.1.97 compatibility * Added node v0.1.97 compatibility
* Added support for deleting cookies via Request#cookie('key', null) * Added support for deleting cookies via Request#cookie('key', null)
* Updated haml submodule * Updated haml submodule
* Fixed not-found page, now using using charset utf-8 * Fixed not-found page, now using charset utf-8
* Fixed show-exceptions page, now using using charset utf-8 * Fixed show-exceptions page, now using charset utf-8
* Fixed view support due to fs.readFile Buffers * Fixed view support due to fs.readFile Buffers
* Changed; mime.type() no longer accepts ".type" due to node extname() changes * Changed; mime.type() no longer accepts ".type" due to node extname() changes
@ -3459,7 +3539,7 @@ Shaw]
================== ==================
* Added charset support via Request#charset (automatically assigned to 'UTF-8' when respond()'s * 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 "encoding" option to Request#render(). Closes #299
* Added "dump exceptions" setting, which is enabled by default. * Added "dump exceptions" setting, which is enabled by default.
* Added simple ejs template engine support * Added simple ejs template engine support
@ -3498,7 +3578,7 @@ Shaw]
* Added [haml.js](http://github.com/visionmedia/haml.js) submodule; removed haml-js * 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 callback function support to Request#halt() as 3rd/4th arg
* Added preprocessing of route param wildcards using param(). Closes #251 * 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 bug preventing falsey params (such as ?page=0). Closes #286
* Fixed setting of multiple cookies. Closes #199 * Fixed setting of multiple cookies. Closes #199
* Changed; view naming convention is now NAME.TYPE.ENGINE (for example page.html.haml) * Changed; view naming convention is now NAME.TYPE.ENGINE (for example page.html.haml)

View File

@ -1,12 +1,10 @@
[![Express Logo](https://i.cloudup.com/zfY6lL7eFa-3000x3000.png)](http://expressjs.com/) [![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-image]][npm-url] [![NPM Version][npm-version-image]][npm-url]
[![NPM Downloads][downloads-image]][downloads-url] [![NPM Install Size][npm-install-size-image]][npm-install-size-url]
[![Linux Build][ci-image]][ci-url] [![NPM Downloads][npm-downloads-image]][npm-downloads-url]
[![Windows Build][appveyor-image]][appveyor-url]
[![Test Coverage][coveralls-image]][coveralls-url]
```js ```js
const express = require('express') const express = require('express')
@ -33,7 +31,7 @@ the [`npm init` command](https://docs.npmjs.com/creating-a-package-json-file).
Installation is done using the Installation is done using the
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
```bash ```console
$ npm install express $ npm install express
``` ```
@ -53,7 +51,7 @@ for more information.
## Docs & Community ## Docs & Community
* [Website and Documentation](http://expressjs.com/) - [[website repo](https://github.com/expressjs/expressjs.com)] * [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 * [GitHub Organization](https://github.com/expressjs) for Official Middleware & Modules
* Visit the [Wiki](https://github.com/expressjs/express/wiki) * Visit the [Wiki](https://github.com/expressjs/express/wiki)
* [Google Group](https://groups.google.com/group/express-js) for discussion * [Google Group](https://groups.google.com/group/express-js) for discussion
@ -61,35 +59,31 @@ for more information.
**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 [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).
### Security Issues
If you discover a security vulnerability in Express, please see [Security Policies and Procedures](Security.md).
## Quick Start ## Quick Start
The quickest way to get started with express is to utilize the executable [`express(1)`](https://github.com/expressjs/generator) to generate an application as shown below: The quickest way to get started with express is to utilize the executable [`express(1)`](https://github.com/expressjs/generator) to generate an application as shown below:
Install the executable. The executable's major version will match Express's: Install the executable. The executable's major version will match Express's:
```bash ```console
$ npm install -g express-generator@4 $ npm install -g express-generator@4
``` ```
Create the app: Create the app:
```bash ```console
$ express /tmp/foo && cd /tmp/foo $ express /tmp/foo && cd /tmp/foo
``` ```
Install dependencies: Install dependencies:
```bash ```console
$ npm install $ npm install
``` ```
Start the server: Start the server:
```bash ```console
$ npm start $ npm start
``` ```
@ -109,30 +103,42 @@ $ npm start
To view the examples, clone the Express repo and install the dependencies: To view the examples, clone the Express repo and install the dependencies:
```bash ```console
$ git clone git://github.com/expressjs/express.git --depth 1 $ git clone https://github.com/expressjs/express.git --depth 1
$ cd express $ cd express
$ npm install $ npm install
``` ```
Then run whichever example you want: Then run whichever example you want:
```bash ```console
$ node examples/content-negotiation $ node examples/content-negotiation
``` ```
## Tests
To run the test suite, first install the dependencies, then run `npm test`:
```bash
$ npm install
$ npm test
```
## Contributing ## Contributing
[Contributing Guide](Contributing.md) [![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,
from code for bug fixes and enhancements, to additions and fixes to documentation, additional
tests, triaging incoming pull requests and issues, and more!
See the [Contributing Guide](Contributing.md) for more technical details on contributing.
### Security Issues
If you discover a security vulnerability in Express, please see [Security Policies and Procedures](Security.md).
### Running Tests
To run the test suite, first install the dependencies, then run `npm test`:
```console
$ npm install
$ npm test
```
## People ## People
@ -146,13 +152,15 @@ The current lead maintainer is [Douglas Christopher Wilson](https://github.com/d
[MIT](LICENSE) [MIT](LICENSE)
[ci-image]: https://img.shields.io/github/workflow/status/expressjs/express/ci/master.svg?label=linux [appveyor-image]: https://badgen.net/appveyor/ci/dougwilson/express/master?label=windows
[ci-url]: https://github.com/expressjs/express/actions?query=workflow%3Aci
[npm-image]: https://img.shields.io/npm/v/express.svg
[npm-url]: https://npmjs.org/package/express
[downloads-image]: https://img.shields.io/npm/dm/express.svg
[downloads-url]: https://npmcharts.com/compare/express?minimal=true
[appveyor-image]: https://img.shields.io/appveyor/ci/dougwilson/express/master.svg?label=windows
[appveyor-url]: https://ci.appveyor.com/project/dougwilson/express [appveyor-url]: https://ci.appveyor.com/project/dougwilson/express
[coveralls-image]: https://img.shields.io/coveralls/expressjs/express/master.svg [coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/express/master
[coveralls-url]: https://coveralls.io/r/expressjs/express?branch=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-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

View File

@ -184,3 +184,9 @@ $ npm publish
**NOTE:** The version number to publish will be picked up automatically from **NOTE:** The version number to publish will be picked up automatically from
package.json. 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,

View File

@ -27,8 +27,7 @@ endeavor to keep you informed of the progress towards a fix and full
announcement, and may ask for additional information or guidance. announcement, and may ask for additional information or guidance.
Report security bugs in third-party modules to the person or team maintaining Report security bugs in third-party modules to the person or team maintaining
the module. You can also report a vulnerability through the the module.
[Node Security Project](https://nodesecurity.io/report).
## Disclosure Policy ## Disclosure Policy

View File

@ -10,18 +10,29 @@ environment:
- nodejs_version: "11.15" - nodejs_version: "11.15"
- nodejs_version: "12.22" - nodejs_version: "12.22"
- nodejs_version: "13.14" - nodejs_version: "13.14"
- nodejs_version: "14.19" - 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: cache:
- node_modules - node_modules
install: install:
# Install Node.js # Install Node.js
- ps: >- - ps: >-
try { Install-Product node $env:nodejs_version -ErrorAction Stop } try { Install-Product node $env:nodejs_version -ErrorAction Stop }
catch { Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) } catch { Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) x64 }
# Configure npm # Configure npm
- ps: | - ps: |
npm config set loglevel error 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 npm config set shrinkwrap false
}
# Remove all non-test dependencies # Remove all non-test dependencies
- ps: | - ps: |
# Remove example dependencies # Remove example dependencies
@ -37,7 +48,10 @@ install:
# - use 6.x for Node.js < 8 # - use 6.x for Node.js < 8
# - use 7.x for Node.js < 10 # - use 7.x for Node.js < 10
# - use 8.x for Node.js < 12 # - use 8.x for Node.js < 12
if ([int]$env:nodejs_version.split(".")[0] -lt 6) { # - 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 npm install --silent --save-dev mocha@5.2.0
} elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) {
npm install --silent --save-dev mocha@6.2.2 npm install --silent --save-dev mocha@6.2.2
@ -45,23 +59,29 @@ install:
npm install --silent --save-dev mocha@7.2.0 npm install --silent --save-dev mocha@7.2.0
} elseif ([int]$env:nodejs_version.split(".")[0] -lt 12) { } elseif ([int]$env:nodejs_version.split(".")[0] -lt 12) {
npm install --silent --save-dev mocha@8.4.0 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: | - ps: |
# nyc for test coverage # nyc for test coverage
# - use 10.3.2 for Node.js < 4 # - use 10.3.2 for Node.js < 4
# - use 11.9.0 for Node.js < 6 # - 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) { if ([int]$env:nodejs_version.split(".")[0] -lt 4) {
npm install --silent --save-dev nyc@10.3.2 npm install --silent --save-dev nyc@10.3.2
} elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) { } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) {
npm install --silent --save-dev nyc@11.9.0 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 npm install --silent --save-dev nyc@14.1.1
} }
- ps: | - ps: |
# supertest for http calls # supertest for http calls
# - use 3.4.2 for Node.js < 6 # - use 2.0.0 for Node.js < 4
if ([int]$env:nodejs_version.split(".")[0] -lt 6) { # - 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 npm install --silent --save-dev supertest@3.4.2
} elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) {
npm install --silent --save-dev supertest@6.1.6 npm install --silent --save-dev supertest@6.1.6

View File

@ -1,13 +1,17 @@
all: all:
@./run 1 middleware @./run 1 middleware 50
@./run 5 middleware @./run 5 middleware 50
@./run 10 middleware @./run 10 middleware 50
@./run 15 middleware @./run 15 middleware 50
@./run 20 middleware @./run 20 middleware 50
@./run 30 middleware @./run 30 middleware 50
@./run 50 middleware @./run 50 middleware 50
@./run 100 middleware @./run 100 middleware 50
@./run 10 middleware 100
@./run 10 middleware 250
@./run 10 middleware 500
@./run 10 middleware 1000
@echo @echo
.PHONY: all .PHONY: all

34
benchmarks/README.md Normal file
View File

@ -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`.

View File

@ -13,7 +13,7 @@ while (n--) {
}); });
} }
app.use(function(req, res, next){ app.use(function(req, res){
res.send('Hello World') res.send('Hello World')
}); });

View File

@ -4,13 +4,15 @@ echo
MW=$1 node $2 & MW=$1 node $2 &
pid=$! pid=$!
echo " $3 connections"
sleep 2 sleep 2
wrk 'http://localhost:3333/?foo[bar]=baz' \ wrk 'http://localhost:3333/?foo[bar]=baz' \
-d 3 \ -d 3 \
-c 50 \ -c $3 \
-t 8 \ -t 8 \
| grep 'Requests/sec' \ | grep 'Requests/sec\|Latency' \
| awk '{ print " " $2 }' | awk '{ print " " $2 }'
kill $pid kill $pid

View File

@ -13,7 +13,6 @@ This page contains list of examples using Express.
- [hello-world](./hello-world) - Simple request handler - [hello-world](./hello-world) - Simple request handler
- [markdown](./markdown) - Markdown as template engine - [markdown](./markdown) - Markdown as template engine
- [multi-router](./multi-router) - Working with multiple Express routers - [multi-router](./multi-router) - Working with multiple Express routers
- [multipart](./multipart) - Accepting multipart-encoded forms
- [mvc](./mvc) - MVC-style controllers - [mvc](./mvc) - MVC-style controllers
- [online](./online) - Tracking online user activity with `online` and `redis` packages - [online](./online) - Tracking online user activity with `online` and `redis` packages
- [params](./params) - Working with route parameters - [params](./params) - Working with route parameters

View File

@ -6,12 +6,12 @@
Try accessing <a href="/restricted">/restricted</a>, then authenticate with "tj" and "foobar". Try accessing <a href="/restricted">/restricted</a>, then authenticate with "tj" and "foobar".
<form method="post" action="/login"> <form method="post" action="/login">
<p> <p>
<label>Username:</label> <label for="username">Username:</label>
<input type="text" name="username"> <input type="text" name="username" id="username">
</p> </p>
<p> <p>
<label>Password:</label> <label for="password">Password:</label>
<input type="text" name="password"> <input type="text" name="password" id="password">
</p> </p>
<p> <p>
<input type="submit" value="Login"> <input type="submit" value="Login">

View File

@ -13,13 +13,10 @@ var app = module.exports = express();
app.use(cookieSession({ secret: 'manny is cool' })); app.use(cookieSession({ secret: 'manny is cool' }));
// do something with the session // do something with the session
app.use(count); app.get('/', function (req, res) {
// custom middleware
function count(req, res) {
req.session.count = (req.session.count || 0) + 1 req.session.count = (req.session.count || 0) + 1
res.send('viewed ' + req.session.count + ' times\n') res.send('viewed ' + req.session.count + ' times\n')
} })
/* istanbul ignore next */ /* istanbul ignore next */
if (!module.parent) { if (!module.parent) {

View File

@ -6,7 +6,6 @@
var express = require('../../'); var express = require('../../');
var path = require('path'); var path = require('path');
var resolvePath = require('resolve-path')
var app = module.exports = express(); var app = module.exports = express();
@ -25,9 +24,7 @@ app.get('/', function(req, res){
// /files/* is accessed via req.params[0] // /files/* is accessed via req.params[0]
// but here we name it :file // but here we name it :file
app.get('/files/:file+', function (req, res, next) { app.get('/files/:file+', function (req, res, next) {
var filePath = resolvePath(FILES_DIR, req.params.file) res.download(req.params.file, { root: FILES_DIR }, function (err) {
res.download(filePath, function (err) {
if (!err) return; // file sent if (!err) return; // file sent
if (err.status !== 404) return next(err); // non-404 error if (err.status !== 404) return next(err); // non-404 error
// file for download not found // file for download not found

View File

@ -26,7 +26,7 @@ function error(err, req, res, next) {
res.send('Internal Server Error'); res.send('Internal Server Error');
} }
app.get('/', function(req, res){ app.get('/', function () {
// Caught and passed down to the errorHandler middleware // Caught and passed down to the errorHandler middleware
throw new Error('something broke!'); throw new Error('something broke!');
}); });

View File

@ -26,7 +26,7 @@ app.engine('md', function(path, options, fn){
app.set('views', path.join(__dirname, 'views')); 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.set('view engine', 'md');
app.get('/', function(req, res){ app.get('/', function(req, res){

View File

@ -1,62 +0,0 @@
'use strict'
/**
* Module dependencies.
*/
var express = require('../..');
var multiparty = require('multiparty');
var format = require('util').format;
var app = module.exports = express();
app.get('/', function(req, res){
res.send('<form method="post" enctype="multipart/form-data">'
+ '<p>Title: <input type="text" name="title" /></p>'
+ '<p>Image: <input type="file" name="image" /></p>'
+ '<p><input type="submit" value="Upload" /></p>'
+ '</form>');
});
app.post('/', function(req, res, next){
// create a form to begin parsing
var form = new multiparty.Form();
var image;
var title;
form.on('error', next);
form.on('close', function(){
res.send(format('\nuploaded %s (%d Kb) as %s'
, image.filename
, image.size / 1024 | 0
, title));
});
// listen on field event for title
form.on('field', function(name, val){
if (name !== 'title') return;
title = val;
});
// listen on part event for image file
form.on('part', function(part){
if (!part.filename) return;
if (part.name !== 'image') return part.resume();
image = {};
image.filename = part.filename;
image.size = 0;
part.on('data', function(buf){
image.size += buf.length;
});
});
// parse the form
form.parse(req);
});
/* istanbul ignore next */
if (!module.parent) {
app.listen(4000);
console.log('Express started on port 4000');
}

View File

@ -4,6 +4,7 @@
* Module dependencies. * Module dependencies.
*/ */
var createError = require('http-errors')
var express = require('../../'); var express = require('../../');
var app = module.exports = express(); var app = module.exports = express();
@ -17,14 +18,6 @@ var users = [
, { name: 'bandit' } , { name: 'bandit' }
]; ];
// Create HTTP error
function createError(status, message) {
var err = new Error(message);
err.status = status;
return err;
}
// Convert :to and :from to integers // Convert :to and :from to integers
app.param(['to', 'from'], function(req, res, next, num, name){ app.param(['to', 'from'], function(req, res, next, num, name){
@ -58,7 +51,7 @@ app.get('/', function(req, res){
* GET :user. * GET :user.
*/ */
app.get('/user/:user', function(req, res, next){ app.get('/user/:user', function (req, res) {
res.send('user ' + req.user.name); res.send('user ' + req.user.name);
}); });
@ -66,7 +59,7 @@ app.get('/user/:user', function(req, res, next){
* GET users :from - :to. * 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 from = req.params.from;
var to = req.params.to; var to = req.params.to;
var names = users.map(function(user){ return user.name; }); var names = users.map(function(user){ return user.name; });

View File

@ -3,7 +3,7 @@
<h1>Editing <%= user.name %></h1> <h1>Editing <%= user.name %></h1>
<div id="user"> <div id="user">
<form action="?_method=put", method="post"> <form action="?_method=put" method="post">
<p> <p>
Name: Name:
<input type="text" value="<%= user.name %>" name="user[name]" /> <input type="text" value="<%= user.name %>" name="user[name]" />

View File

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Search example</title> <title>Search example</title>
<style type="text/css"> <style>
body { body {
font: 14px "Helvetica Neue", Helvetica; font: 14px "Helvetica Neue", Helvetica;
padding: 50px; padding: 50px;
@ -15,7 +15,7 @@
<h2>Search</h2> <h2>Search</h2>
<p>Try searching for "ferret" or "cat".</p> <p>Try searching for "ferret" or "cat".</p>
<input type="search" name="search" value="" /> <input type="search" name="search" value="" />
<pre /> <pre></pre>
<script src="/client.js" charset="utf-8"></script> <script src="/client.js" charset="utf-8"></script>
</body> </body>
</html> </html>

View File

@ -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', { res.render('index', {
title: 'Users', title: 'Users',
count: req.count, 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 // you can see now how we have much less
// to pass to res.render(). If we have // to pass to res.render(). If we have
// several routes related to users this // several routes related to users this

View File

@ -72,12 +72,12 @@ var userRepos = {
// and simply expose the data // and simply expose the data
// example: http://localhost:3000/api/users/?api-key=foo // 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); res.send(users);
}); });
// example: http://localhost:3000/api/repos/?api-key=foo // 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); res.send(repos);
}); });

View File

@ -26,6 +26,13 @@ var merge = require('utils-merge');
var resolve = require('path').resolve; var resolve = require('path').resolve;
var Router = require('router'); var Router = require('router');
var setPrototypeOf = require('setprototypeof') var setPrototypeOf = require('setprototypeof')
/**
* Module variables.
* @private
*/
var hasOwnProperty = Object.prototype.hasOwnProperty
var slice = Array.prototype.slice; var slice = Array.prototype.slice;
/** /**
@ -346,7 +353,17 @@ app.param = function param(name, fn) {
app.set = function set(setting, val) { app.set = function set(setting, val) {
if (arguments.length === 1) { if (arguments.length === 1) {
// app.get(setting) // app.get(setting)
return this.settings[setting]; var settings = this.settings
while (settings && settings !== Object.prototype) {
if (hasOwnProperty.call(settings, setting)) {
return settings[setting]
}
settings = Object.getPrototypeOf(settings)
}
return undefined
} }
debug('set "%s" to %o', setting, val); debug('set "%s" to %o', setting, val);

View File

@ -14,6 +14,8 @@
var Buffer = require('safe-buffer').Buffer var Buffer = require('safe-buffer').Buffer
var contentDisposition = require('content-disposition'); var contentDisposition = require('content-disposition');
var createError = require('http-errors')
var deprecate = require('depd')('express');
var encodeUrl = require('encodeurl'); var encodeUrl = require('encodeurl');
var escapeHtml = require('escape-html'); var escapeHtml = require('escape-html');
var http = require('http'); var http = require('http');
@ -32,6 +34,7 @@ var send = require('send');
var extname = path.extname; var extname = path.extname;
var resolve = path.resolve; var resolve = path.resolve;
var vary = require('vary'); var vary = require('vary');
var urlParse = require('url').parse;
/** /**
* Response prototype. * Response prototype.
@ -56,6 +59,9 @@ module.exports = res
*/ */
res.status = function status(code) { 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')
}
this.statusCode = code; this.statusCode = code;
return this; return this;
}; };
@ -180,6 +186,13 @@ res.send = function send(body) {
chunk = ''; chunk = '';
} }
// alter headers for 205
if (this.statusCode === 205) {
this.set('Content-Length', '0')
this.removeHeader('Transfer-Encoding')
chunk = ''
}
if (req.method === 'HEAD') { if (req.method === 'HEAD') {
// skip body for HEAD // skip body for HEAD
this.end(); this.end();
@ -293,7 +306,7 @@ res.jsonp = function jsonp(obj) {
*/ */
res.sendStatus = function sendStatus(statusCode) { res.sendStatus = function sendStatus(statusCode) {
var body = statuses[statusCode] || String(statusCode) var body = statuses.message[statusCode] || String(statusCode)
this.statusCode = statusCode; this.statusCode = statusCode;
this.type('txt'); this.type('txt');
@ -416,6 +429,13 @@ res.download = function download (path, filename, options, callback) {
opts = null opts = null
} }
// support optional filename, where options may be in it's place
if (typeof filename === 'object' &&
(typeof options === 'function' || options === undefined)) {
name = null
opts = filename
}
// set Content-Disposition when file is sent // set Content-Disposition when file is sent
var headers = { var headers = {
'Content-Disposition': contentDisposition(name || path) 'Content-Disposition': contentDisposition(name || path)
@ -437,7 +457,9 @@ res.download = function download (path, filename, options, callback) {
opts.headers = headers opts.headers = headers
// Resolve the full path for sendFile // Resolve the full path for sendFile
var fullPath = resolve(path); var fullPath = !opts.root
? resolve(path)
: path
// send file // send file
return this.sendFile(fullPath, opts, done) return this.sendFile(fullPath, opts, done)
@ -532,9 +554,8 @@ res.format = function(obj){
var req = this.req; var req = this.req;
var next = req.next; var next = req.next;
var fn = obj.default; var keys = Object.keys(obj)
if (fn) delete obj.default; .filter(function (v) { return v !== 'default' })
var keys = Object.keys(obj);
var key = keys.length > 0 var key = keys.length > 0
? req.accepts(keys) ? req.accepts(keys)
@ -545,13 +566,12 @@ res.format = function(obj){
if (key) { if (key) {
this.set('Content-Type', normalizeType(key).value); this.set('Content-Type', normalizeType(key).value);
obj[key](req, this, next); obj[key](req, this, next);
} else if (fn) { } else if (obj.default) {
fn(); obj.default(req, this, next)
} else { } else {
var err = new Error('Not Acceptable'); next(createError(406, {
err.status = err.statusCode = 406; types: normalizeTypes(keys).map(function (o) { return o.value })
err.types = normalizeTypes(keys).map(function(o){ return o.value }); }))
next(err);
} }
return this; return this;
@ -717,9 +737,13 @@ res.cookie = function (name, value, options) {
val = 's:' + sign(val, secret); val = 's:' + sign(val, secret);
} }
if ('maxAge' in opts) { if (opts.maxAge != null) {
opts.expires = new Date(Date.now() + opts.maxAge); var maxAge = opts.maxAge - 0
opts.maxAge /= 1000;
if (!isNaN(maxAge)) {
opts.expires = new Date(Date.now() + maxAge)
opts.maxAge = Math.floor(maxAge / 1000)
}
} }
if (opts.path == null) { if (opts.path == null) {
@ -756,8 +780,25 @@ res.location = function location(url) {
loc = this.req.get('Referrer') || '/'; loc = this.req.get('Referrer') || '/';
} }
var lowerLoc = loc.toLowerCase();
var encodedUrl = encodeUrl(loc);
if (lowerLoc.indexOf('https://') === 0 || lowerLoc.indexOf('http://') === 0) {
try {
var parsedUrl = urlParse(loc);
var parsedEncodedUrl = urlParse(encodedUrl);
// Because this can encode the host, check that we did not change the host
if (parsedUrl.host !== parsedEncodedUrl.host) {
// If the host changes after encodeUrl, return the original url
return this.set('Location', loc);
}
} catch (e) {
// If parse fails, return the original url
return this.set('Location', loc);
}
}
// set location // set location
return this.set('Location', encodeUrl(loc)); return this.set('Location', encodedUrl);
}; };
/** /**
@ -795,12 +836,12 @@ res.redirect = function redirect(url) {
// Support text/{plain,html} by default // Support text/{plain,html} by default
this.format({ this.format({
text: function(){ text: function(){
body = statuses[status] + '. Redirecting to ' + address body = statuses.message[status] + '. Redirecting to ' + address
}, },
html: function(){ html: function(){
var u = escapeHtml(address); var u = escapeHtml(address);
body = '<p>' + statuses[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>' body = '<p>' + statuses.message[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>'
}, },
default: function(){ default: function(){
@ -969,7 +1010,7 @@ function sendfile(res, file, options, callback) {
* ability to escape characters that can trigger HTML sniffing. * ability to escape characters that can trigger HTML sniffing.
* *
* @param {*} value * @param {*} value
* @param {function} replaces * @param {function} replacer
* @param {number} spaces * @param {number} spaces
* @param {boolean} escape * @param {boolean} escape
* @returns {string} * @returns {string}

View File

@ -77,16 +77,15 @@ exports.normalizeTypes = function(types){
/** /**
* Parse accept params `str` returning an * Parse accept params `str` returning an
* object with `.value`, `.quality` and `.params`. * object with `.value`, `.quality` and `.params`.
* also includes `.originalIndex` for stable sorting
* *
* @param {String} str * @param {String} str
* @return {Object} * @return {Object}
* @api private * @api private
*/ */
function acceptParams(str, index) { function acceptParams (str) {
var parts = str.split(/ *; */); 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) { for (var i = 1; i < parts.length; ++i) {
var pms = parts[i].split(/ *= */); var pms = parts[i].split(/ *= */);
@ -240,6 +239,7 @@ function createETagGenerator (options) {
/** /**
* Parse an extended query string with qs. * Parse an extended query string with qs.
* *
* @param {String} str
* @return {Object} * @return {Object}
* @private * @private
*/ */

View File

@ -74,7 +74,7 @@ function View(name, options) {
if (!opts.engines[this.ext]) { if (!opts.engines[this.ext]) {
// load engine // load engine
var mod = this.ext.substr(1) var mod = this.ext.slice(1)
debug('require "%s"', mod) debug('require "%s"', mod)
// default engine export // default engine export

View File

@ -28,35 +28,36 @@
"api" "api"
], ],
"dependencies": { "dependencies": {
"accepts": "~1.3.7", "accepts": "~1.3.8",
"array-flatten": "3.0.0", "array-flatten": "3.0.0",
"body-parser": "2.0.0-beta.1", "body-parser": "2.0.0-beta.2",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.4.2", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "3.1.0", "debug": "3.1.0",
"depd": "~1.1.2", "depd": "2.0.0",
"encodeurl": "~1.0.2", "encodeurl": "~1.0.2",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "~1.1.2", "finalhandler": "1.2.0",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1", "merge-descriptors": "1.0.1",
"methods": "~1.1.2", "methods": "~1.1.2",
"mime-types": "~2.1.34", "mime-types": "~2.1.34",
"on-finished": "~2.3.0", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-is-absolute": "1.0.1", "path-is-absolute": "1.0.1",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.9.7", "qs": "6.11.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"router": "2.0.0-beta.1", "router": "2.0.0-beta.2",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "1.0.0-beta.1", "send": "1.0.0-beta.2",
"serve-static": "2.0.0-beta.1", "serve-static": "2.0.0-beta.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "~1.5.0", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"utils-merge": "1.0.1", "utils-merge": "1.0.1",
"vary": "~1.1.2" "vary": "~1.1.2"
@ -66,20 +67,17 @@
"connect-redis": "3.4.2", "connect-redis": "3.4.2",
"cookie-parser": "1.4.6", "cookie-parser": "1.4.6",
"cookie-session": "2.0.0", "cookie-session": "2.0.0",
"ejs": "3.1.6", "ejs": "3.1.9",
"eslint": "7.32.0", "eslint": "8.47.0",
"express-session": "1.17.2", "express-session": "1.17.2",
"hbs": "4.2.0", "hbs": "4.2.0",
"marked": "0.7.0", "marked": "0.7.0",
"method-override": "3.0.0", "method-override": "3.0.0",
"mocha": "9.2.0", "mocha": "10.2.0",
"morgan": "1.10.0", "morgan": "1.10.0",
"multiparty": "4.2.3",
"nyc": "15.1.0", "nyc": "15.1.0",
"pbkdf2-password": "1.2.1", "pbkdf2-password": "1.2.1",
"resolve-path": "1.4.0", "supertest": "6.3.0",
"should": "13.2.3",
"supertest": "6.2.2",
"vhost": "~3.0.2" "vhost": "~3.0.2"
}, },
"engines": { "engines": {

View File

@ -1,7 +1,7 @@
'use strict' 'use strict'
var after = require('after'); var after = require('after');
var should = require('should'); var assert = require('assert')
var express = require('../') var express = require('../')
, Route = express.Route , Route = express.Route
, methods = require('methods') , methods = require('methods')
@ -13,6 +13,37 @@ describe('Route', function(){
route.dispatch(req, {}, done) route.dispatch(req, {}, done)
}) })
it('should not stack overflow with a large sync stack', function (done) {
this.timeout(5000) // long-running test
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) {
req.counter++
next()
})
}
route.get(function (req, res, next) {
req.called = true
next()
})
route.dispatch(req, {}, function (err) {
if (err) return done(err)
assert.ok(req.called)
assert.strictEqual(req.counter, 6000)
done()
})
})
describe('.all', function(){ describe('.all', function(){
it('should add handler', function(done){ it('should add handler', function(done){
var req = { method: 'GET', url: '/' }; var req = { method: 'GET', url: '/' };
@ -25,7 +56,7 @@ describe('Route', function(){
route.dispatch(req, {}, function (err) { route.dispatch(req, {}, function (err) {
if (err) return done(err); if (err) return done(err);
should(req.called).be.ok() assert.ok(req.called)
done(); done();
}); });
}) })
@ -35,7 +66,7 @@ describe('Route', function(){
var route = new Route('/foo'); var route = new Route('/foo');
var cb = after(methods.length, function (err) { var cb = after(methods.length, function (err) {
if (err) return done(err); if (err) return done(err);
count.should.equal(methods.length); assert.strictEqual(count, methods.length)
done(); done();
}); });
@ -66,7 +97,7 @@ describe('Route', function(){
route.dispatch(req, {}, function (err) { route.dispatch(req, {}, function (err) {
if (err) return done(err); if (err) return done(err);
req.count.should.equal(2); assert.strictEqual(req.count, 2)
done(); done();
}); });
}) })
@ -84,7 +115,7 @@ describe('Route', function(){
route.dispatch(req, {}, function (err) { route.dispatch(req, {}, function (err) {
if (err) return done(err); if (err) return done(err);
should(req.called).be.ok() assert.ok(req.called)
done(); done();
}); });
}) })
@ -93,7 +124,7 @@ describe('Route', function(){
var req = { method: 'POST', url: '/' }; var req = { method: 'POST', url: '/' };
var route = new Route(''); var route = new Route('');
route.get(function(req, res, next) { route.get(function () {
throw new Error('not me!'); throw new Error('not me!');
}) })
@ -104,7 +135,7 @@ describe('Route', function(){
route.dispatch(req, {}, function (err) { route.dispatch(req, {}, function (err) {
if (err) return done(err); if (err) return done(err);
should(req.called).be.true() assert.ok(req.called)
done(); done();
}); });
}) })
@ -130,7 +161,7 @@ describe('Route', function(){
route.dispatch(req, {}, function (err) { route.dispatch(req, {}, function (err) {
if (err) return done(err); if (err) return done(err);
req.order.should.equal('abc'); assert.strictEqual(req.order, 'abc')
done(); done();
}); });
}) })
@ -156,9 +187,9 @@ describe('Route', function(){
}); });
route.dispatch(req, {}, function (err) { route.dispatch(req, {}, function (err) {
should(err).be.ok() assert.ok(err)
should(err.message).equal('foobar'); assert.strictEqual(err.message, 'foobar')
req.order.should.equal('a'); assert.strictEqual(req.order, 'a')
done(); done();
}); });
}) })
@ -167,7 +198,7 @@ describe('Route', function(){
var req = { order: '', method: 'GET', url: '/' }; var req = { order: '', method: 'GET', url: '/' };
var route = new Route(''); var route = new Route('');
route.all(function(req, res, next){ route.all(function () {
throw new Error('foobar'); throw new Error('foobar');
}); });
@ -182,9 +213,9 @@ describe('Route', function(){
}); });
route.dispatch(req, {}, function (err) { route.dispatch(req, {}, function (err) {
should(err).be.ok() assert.ok(err)
should(err.message).equal('foobar'); assert.strictEqual(err.message, 'foobar')
req.order.should.equal('a'); assert.strictEqual(req.order, 'a')
done(); done();
}); });
}); });
@ -193,7 +224,7 @@ describe('Route', function(){
var req = { method: 'GET', url: '/' }; var req = { method: 'GET', url: '/' };
var route = new Route(''); var route = new Route('');
route.get(function(req, res, next){ route.get(function () {
throw new Error('boom!'); throw new Error('boom!');
}); });
@ -208,7 +239,7 @@ describe('Route', function(){
route.dispatch(req, {}, function (err) { route.dispatch(req, {}, function (err) {
if (err) return done(err); if (err) return done(err);
should(req.message).equal('oops'); assert.strictEqual(req.message, 'oops')
done(); done();
}); });
}); });
@ -222,8 +253,8 @@ describe('Route', function(){
}); });
route.dispatch(req, {}, function(err){ route.dispatch(req, {}, function(err){
should(err).be.ok() assert.ok(err)
err.message.should.equal('boom!'); assert.strictEqual(err.message, 'boom!')
done(); done();
}); });
}); });
@ -234,7 +265,7 @@ describe('Route', function(){
route.all(function(err, req, res, next){ route.all(function(err, req, res, next){
// this should not execute // this should not execute
true.should.be.false() throw new Error('should not be called')
}); });
route.dispatch(req, {}, done); route.dispatch(req, {}, done);

View File

@ -61,7 +61,36 @@ describe('Router', function(){
router.handle({ method: 'GET' }, {}, done) 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){ 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(); var router = new Router();
@ -76,6 +105,60 @@ describe('Router', function(){
router.handle({ url: '/', method: 'GET' }, { end: done }, function(){}); router.handle({ url: '/', method: 'GET' }, { end: done }, function(){});
}); });
it('should not stack overflow with a large sync route stack', function (done) {
this.timeout(5000) // long-running test
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) {
req.counter++
next()
})
}
router.get('/foo', function (req, res) {
assert.strictEqual(req.counter, 6000)
res.end()
})
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) {
this.timeout(5000) // long-running test
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) {
req.counter++
next()
})
}
router.use(function (req, res) {
assert.strictEqual(req.counter, 6000)
res.end()
})
router.handle({ url: '/', method: 'GET' }, { end: done }, function (err) {
assert(!err, err);
})
})
describe('.handle', function(){ describe('.handle', function(){
it('should dispatch', function(done){ it('should dispatch', function(done){
var router = new Router(); var router = new Router();
@ -149,7 +232,7 @@ describe('Router', function(){
it('should handle throwing inside routes with params', function(done) { it('should handle throwing inside routes with params', function(done) {
var router = new Router(); var router = new Router();
router.get('/foo/:id', function(req, res, next){ router.get('/foo/:id', function () {
throw new Error('foo'); throw new Error('foo');
}); });
@ -519,8 +602,8 @@ describe('Router', function(){
var req2 = { url: '/foo/10/bar', method: 'get' }; var req2 = { url: '/foo/10/bar', method: 'get' };
var router = new Router(); var router = new Router();
var sub = new Router(); var sub = new Router();
var cb = after(2, done)
done = after(2, done);
sub.get('/bar', function(req, res, next) { sub.get('/bar', function(req, res, next) {
next(); next();
@ -539,14 +622,14 @@ describe('Router', function(){
assert.ifError(err); assert.ifError(err);
assert.equal(req1.ms, 50); assert.equal(req1.ms, 50);
assert.equal(req1.originalUrl, '/foo/50/bar'); assert.equal(req1.originalUrl, '/foo/50/bar');
done(); cb()
}); });
router.handle(req2, {}, function(err) { router.handle(req2, {}, function(err) {
assert.ifError(err); assert.ifError(err);
assert.equal(req2.ms, 10); assert.equal(req2.ms, 10);
assert.equal(req2.originalUrl, '/foo/10/bar'); assert.equal(req2.originalUrl, '/foo/10/bar');
done(); cb()
}); });
}); });
}); });

View File

@ -6,9 +6,8 @@ describe('app.listen()', function(){
it('should wrap with an HTTP server', function(done){ it('should wrap with an HTTP server', function(done){
var app = express(); var app = express();
var server = app.listen(9999, function(){ var server = app.listen(0, function () {
server.close(); server.close(done)
done();
}); });
}) })
}) })

View File

@ -2,28 +2,24 @@
var assert = require('assert') var assert = require('assert')
var express = require('../') var express = require('../')
var should = require('should')
describe('app', function(){ describe('app', function(){
describe('.locals(obj)', function(){ describe('.locals', function () {
it('should merge locals', function(){ it('should default object', function () {
var app = express(); var app = express()
should(Object.keys(app.locals)).eql(['settings']) assert.ok(app.locals)
app.locals.user = 'tobi'; assert.strictEqual(typeof app.locals, 'object')
app.locals.age = 2;
should(Object.keys(app.locals)).eql(['settings', 'user', 'age'])
assert.strictEqual(app.locals.user, 'tobi')
assert.strictEqual(app.locals.age, 2)
})
}) })
describe('.locals.settings', function(){ describe('.settings', function () {
it('should expose app settings', function(){ it('should contain app settings ', function () {
var app = express(); var app = express()
app.set('title', 'House of Manny'); app.set('title', 'Express')
var obj = app.locals.settings; assert.ok(app.locals.settings)
should(obj).have.property('env', 'test') assert.strictEqual(typeof app.locals.settings, 'object')
should(obj).have.property('title', 'House of Manny') assert.strictEqual(app.locals.settings, app.settings)
assert.strictEqual(app.locals.settings.title, 'Express')
})
}) })
}) })
}) })

View File

@ -124,7 +124,7 @@ describe('app', function(){
app.get('/:user', function(req, res, next) { app.get('/:user', function(req, res, next) {
next('route'); next('route');
}); });
app.get('/:user', function(req, res, next) { app.get('/:user', function (req, res) {
res.send(req.params.user); res.send(req.params.user);
}); });
@ -145,11 +145,11 @@ describe('app', function(){
next(new Error('invalid invocation')) next(new Error('invalid invocation'))
}); });
app.post('/:user', function(req, res, next) { app.post('/:user', function (req, res) {
res.send(req.params.user); res.send(req.params.user);
}); });
app.get('/:thing', function(req, res, next) { app.get('/:thing', function (req, res) {
res.send(req.thing); res.send(req.thing);
}); });

View File

@ -92,7 +92,7 @@ describe('app.router', function(){
it('should decode correct params', function(done){ it('should decode correct params', function(done){
var app = express(); var app = express();
app.get('/:name', function(req, res, next){ app.get('/:name', function (req, res) {
res.send(req.params.name); res.send(req.params.name);
}); });
@ -104,7 +104,7 @@ describe('app.router', function(){
it('should not accept params in malformed paths', function(done) { it('should not accept params in malformed paths', function(done) {
var app = express(); var app = express();
app.get('/:name', function(req, res, next){ app.get('/:name', function (req, res) {
res.send(req.params.name); res.send(req.params.name);
}); });
@ -116,7 +116,7 @@ describe('app.router', function(){
it('should not decode spaces', function(done) { it('should not decode spaces', function(done) {
var app = express(); var app = express();
app.get('/:name', function(req, res, next){ app.get('/:name', function (req, res) {
res.send(req.params.name); res.send(req.params.name);
}); });
@ -128,7 +128,7 @@ describe('app.router', function(){
it('should work with unicode', function(done) { it('should work with unicode', function(done) {
var app = express(); var app = express();
app.get('/:name', function(req, res, next){ app.get('/:name', function (req, res) {
res.send(req.params.name); res.send(req.params.name);
}); });
@ -791,7 +791,7 @@ describe('app.router', function(){
request(app) request(app)
.get('/foo.json') .get('/foo.json')
.expect(200, 'foo as json', done) .expect(200, 'foo as json', cb)
}) })
}) })
@ -805,7 +805,7 @@ describe('app.router', function(){
next(); next();
}); });
app.get('/bar', function(req, res){ app.get('/bar', function () {
assert(0); assert(0);
}); });
@ -814,7 +814,7 @@ describe('app.router', function(){
next(); next();
}); });
app.get('/foo', function(req, res, next){ app.get('/foo', function (req, res) {
calls.push('/foo 2'); calls.push('/foo 2');
res.json(calls) res.json(calls)
}); });
@ -834,7 +834,7 @@ describe('app.router', function(){
next('route') next('route')
} }
app.get('/foo', fn, function(req, res, next){ app.get('/foo', fn, function (req, res) {
res.end('failure') res.end('failure')
}); });
@ -859,11 +859,11 @@ describe('app.router', function(){
next('router') next('router')
} }
router.get('/foo', fn, function (req, res, next) { router.get('/foo', fn, function (req, res) {
res.end('failure') res.end('failure')
}) })
router.get('/foo', function (req, res, next) { router.get('/foo', function (req, res) {
res.end('failure') res.end('failure')
}) })
@ -890,7 +890,7 @@ describe('app.router', function(){
next(); next();
}); });
app.get('/bar', function(req, res){ app.get('/bar', function () {
assert(0); assert(0);
}); });
@ -899,7 +899,7 @@ describe('app.router', function(){
next(new Error('fail')); next(new Error('fail'));
}); });
app.get('/foo', function(req, res, next){ app.get('/foo', function () {
assert(0); assert(0);
}); });

View File

@ -57,7 +57,7 @@ describe('app', function(){
request(app) request(app)
.get('/forum') .get('/forum')
.expect(200, 'forum', done) .expect(200, 'forum', cb)
}) })
it('should set the child\'s .parent', function(){ it('should set the child\'s .parent', function(){

View File

@ -11,6 +11,12 @@ describe('config', function () {
assert.equal(app.get('foo'), 'bar'); assert.equal(app.get('foo'), 'bar');
}) })
it('should set prototype values', function () {
var app = express()
app.set('hasOwnProperty', 42)
assert.strictEqual(app.get('hasOwnProperty'), 42)
})
it('should return the app', function () { it('should return the app', function () {
var app = express(); var app = express();
assert.equal(app.set('foo', 'bar'), app); assert.equal(app.set('foo', 'bar'), app);
@ -21,6 +27,17 @@ describe('config', function () {
assert.equal(app.set('foo', undefined), app); assert.equal(app.set('foo', undefined), app);
}) })
it('should return set value', function () {
var app = express()
app.set('foo', 'bar')
assert.strictEqual(app.set('foo'), 'bar')
})
it('should return undefined for prototype values', function () {
var app = express()
assert.strictEqual(app.set('hasOwnProperty'), undefined)
})
describe('"etag"', function(){ describe('"etag"', function(){
it('should throw on bad value', function(){ it('should throw on bad value', function(){
var app = express(); var app = express();
@ -51,6 +68,11 @@ describe('config', function () {
assert.strictEqual(app.get('foo'), undefined); assert.strictEqual(app.get('foo'), undefined);
}) })
it('should return undefined for prototype values', function () {
var app = express()
assert.strictEqual(app.get('hasOwnProperty'), undefined)
})
it('should otherwise return the value', function(){ it('should otherwise return the value', function(){
var app = express(); var app = express();
app.set('foo', 'bar'); app.set('foo', 'bar');
@ -125,6 +147,12 @@ describe('config', function () {
assert.equal(app.enable('tobi'), app); assert.equal(app.enable('tobi'), app);
assert.strictEqual(app.get('tobi'), true); assert.strictEqual(app.get('tobi'), true);
}) })
it('should set prototype values', function () {
var app = express()
app.enable('hasOwnProperty')
assert.strictEqual(app.get('hasOwnProperty'), true)
})
}) })
describe('.disable()', function(){ describe('.disable()', function(){
@ -133,6 +161,12 @@ describe('config', function () {
assert.equal(app.disable('tobi'), app); assert.equal(app.disable('tobi'), app);
assert.strictEqual(app.get('tobi'), false); assert.strictEqual(app.get('tobi'), false);
}) })
it('should set prototype values', function () {
var app = express()
app.disable('hasOwnProperty')
assert.strictEqual(app.get('hasOwnProperty'), false)
})
}) })
describe('.enabled()', function(){ describe('.enabled()', function(){
@ -146,6 +180,11 @@ describe('config', function () {
app.set('foo', 'bar'); app.set('foo', 'bar');
assert.strictEqual(app.enabled('foo'), true); assert.strictEqual(app.enabled('foo'), true);
}) })
it('should default to false for prototype values', function () {
var app = express()
assert.strictEqual(app.enabled('hasOwnProperty'), false)
})
}) })
describe('.disabled()', function(){ describe('.disabled()', function(){
@ -159,5 +198,10 @@ describe('config', function () {
app.set('foo', 'bar'); app.set('foo', 'bar');
assert.strictEqual(app.disabled('foo'), false); assert.strictEqual(app.disabled('foo'), false);
}) })
it('should default to true for prototype values', function () {
var app = express()
assert.strictEqual(app.disabled('hasOwnProperty'), true)
})
}) })
}) })

View File

@ -6,7 +6,7 @@ var request = require('supertest');
describe('exports', function(){ describe('exports', function(){
it('should expose Router', function(){ it('should expose Router', function(){
express.Router.should.be.a.Function() assert.strictEqual(typeof express.Router, 'function')
}) })
it('should expose json middleware', function () { it('should expose json middleware', function () {
@ -35,20 +35,23 @@ describe('exports', function(){
}) })
it('should expose the application prototype', function(){ it('should expose the application prototype', function(){
express.application.set.should.be.a.Function() assert.strictEqual(typeof express.application, 'object')
assert.strictEqual(typeof express.application.set, 'function')
}) })
it('should expose the request prototype', function(){ it('should expose the request prototype', function(){
express.request.accepts.should.be.a.Function() assert.strictEqual(typeof express.request, 'object')
assert.strictEqual(typeof express.request.accepts, 'function')
}) })
it('should expose the response prototype', function(){ it('should expose the response prototype', function(){
express.response.send.should.be.a.Function() assert.strictEqual(typeof express.response, 'object')
assert.strictEqual(typeof express.response.send, 'function')
}) })
it('should permit modifying the .application prototype', function(){ it('should permit modifying the .application prototype', function(){
express.application.foo = function(){ return 'bar'; }; express.application.foo = function(){ return 'bar'; };
express().foo().should.equal('bar'); assert.strictEqual(express().foo(), 'bar')
}) })
it('should permit modifying the .request prototype', function(done){ it('should permit modifying the .request prototype', function(done){

View File

@ -1,10 +1,15 @@
'use strict' 'use strict'
var assert = require('assert') var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer var Buffer = require('safe-buffer').Buffer
var express = require('..') var express = require('..')
var request = require('supertest') var request = require('supertest')
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('express.json()', function () { describe('express.json()', function () {
it('should parse JSON', function (done) { it('should parse JSON', function (done) {
request(createApp()) request(createApp())
@ -38,6 +43,15 @@ describe('express.json()', function () {
.expect(200, '{}', done) .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(' \n'), done)
})
it('should 400 when invalid content-length', function (done) { it('should 400 when invalid content-length', function (done) {
var app = express() var app = express()
@ -86,7 +100,7 @@ describe('express.json()', function () {
.post('/') .post('/')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.send('{:') .send('{:')
.expect(400, parseError('{:'), done) .expect(400, '[entity.parse.failed] ' + parseError('{:'), done)
}) })
it('should 400 for incomplete', function (done) { it('should 400 for incomplete', function (done) {
@ -94,16 +108,7 @@ describe('express.json()', function () {
.post('/') .post('/')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.send('{"user"') .send('{"user"')
.expect(400, parseError('{"user"'), done) .expect(400, '[entity.parse.failed] ' + parseError('{"user"'), done)
})
it('should error with type = "entity.parse.failed"', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'application/json')
.set('X-Error-Property', 'type')
.send(' {"user"')
.expect(400, 'entity.parse.failed', done)
}) })
it('should include original body on error object', function (done) { it('should include original body on error object', function (done) {
@ -124,24 +129,13 @@ describe('express.json()', function () {
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.set('Content-Length', '1034') .set('Content-Length', '1034')
.send(JSON.stringify({ str: buf.toString() })) .send(JSON.stringify({ str: buf.toString() }))
.expect(413, done) .expect(413, '[entity.too.large] request entity too large', done)
})
it('should error with type = "entity.too.large"', function (done) {
var buf = Buffer.alloc(1024, '.')
request(createApp({ limit: '1kb' }))
.post('/')
.set('Content-Type', 'application/json')
.set('Content-Length', '1034')
.set('X-Error-Property', 'type')
.send(JSON.stringify({ str: buf.toString() }))
.expect(413, 'entity.too.large', done)
}) })
it('should 413 when over limit with chunked encoding', function (done) { it('should 413 when over limit with chunked encoding', function (done) {
var app = createApp({ limit: '1kb' })
var buf = Buffer.alloc(1024, '.') var buf = Buffer.alloc(1024, '.')
var server = createApp({ limit: '1kb' }) var test = request(app).post('/')
var test = request(server).post('/')
test.set('Content-Type', 'application/json') test.set('Content-Type', 'application/json')
test.set('Transfer-Encoding', 'chunked') test.set('Transfer-Encoding', 'chunked')
test.write('{"str":') test.write('{"str":')
@ -149,6 +143,15 @@ describe('express.json()', function () {
test.expect(413, done) test.expect(413, done)
}) })
it('should 413 when inflated body over limit', function (done) {
var app = createApp({ limit: '1kb' })
var test = request(app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/json')
test.write(Buffer.from('1f8b080000000000000aab562a2e2952b252d21b05a360148c58a0540b0066f7ce1e0a040000', 'hex'))
test.expect(413, done)
})
it('should accept number of bytes', function (done) { it('should accept number of bytes', function (done) {
var buf = Buffer.alloc(1024, '.') var buf = Buffer.alloc(1024, '.')
request(createApp({ limit: 1024 })) request(createApp({ limit: 1024 }))
@ -161,11 +164,11 @@ describe('express.json()', function () {
it('should not change when options altered', function (done) { it('should not change when options altered', function (done) {
var buf = Buffer.alloc(1024, '.') var buf = Buffer.alloc(1024, '.')
var options = { limit: '1kb' } var options = { limit: '1kb' }
var server = createApp(options) var app = createApp(options)
options.limit = '100kb' options.limit = '100kb'
request(server) request(app)
.post('/') .post('/')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.send(JSON.stringify({ str: buf.toString() })) .send(JSON.stringify({ str: buf.toString() }))
@ -174,14 +177,23 @@ describe('express.json()', function () {
it('should not hang response', function (done) { it('should not hang response', function (done) {
var buf = Buffer.alloc(10240, '.') var buf = Buffer.alloc(10240, '.')
var server = createApp({ limit: '8kb' }) var app = createApp({ limit: '8kb' })
var test = request(server).post('/') var test = request(app).post('/')
test.set('Content-Type', 'application/json') test.set('Content-Type', 'application/json')
test.write(buf) test.write(buf)
test.write(buf) test.write(buf)
test.write(buf) test.write(buf)
test.expect(413, done) test.expect(413, done)
}) })
it('should not error when inflating', function (done) {
var app = createApp({ limit: '1kb' })
var test = request(app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/json')
test.write(Buffer.from('1f8b080000000000000aab562a2e2952b252d21b05a360148c58a0540b0066f7ce1e0a0400', 'hex'))
test.expect(413, done)
})
}) })
describe('with inflate option', function () { describe('with inflate option', function () {
@ -195,7 +207,7 @@ describe('express.json()', function () {
test.set('Content-Encoding', 'gzip') test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/json') test.set('Content-Type', 'application/json')
test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex'))
test.expect(415, 'content encoding unsupported', done) test.expect(415, '[encoding.unsupported] content encoding unsupported', done)
}) })
}) })
@ -225,7 +237,7 @@ describe('express.json()', function () {
.post('/') .post('/')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.send('true') .send('true')
.expect(400, parseError('#rue').replace('#', 't'), done) .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace(/#/g, 't'), done)
}) })
}) })
@ -253,7 +265,7 @@ describe('express.json()', function () {
.post('/') .post('/')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.send('true') .send('true')
.expect(400, 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) { it('should not parse primitives with leading whitespaces', function (done) {
@ -261,7 +273,7 @@ describe('express.json()', function () {
.post('/') .post('/')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.send(' true') .send(' true')
.expect(400, 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) { it('should allow leading whitespaces in JSON', function (done) {
@ -272,15 +284,6 @@ describe('express.json()', function () {
.expect(200, '{"user":"tobi"}', done) .expect(200, '{"user":"tobi"}', done)
}) })
it('should error with type = "entity.parse.failed"', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'application/json')
.set('X-Error-Property', 'type')
.send('true')
.expect(400, 'entity.parse.failed', done)
})
it('should include correct message in stack trace', function (done) { it('should include correct message in stack trace', function (done) {
request(this.app) request(this.app)
.post('/') .post('/')
@ -288,7 +291,7 @@ describe('express.json()', function () {
.set('X-Error-Property', 'stack') .set('X-Error-Property', 'stack')
.send('true') .send('true')
.expect(400) .expect(400)
.expect(shouldContainInBody(parseError('#rue').replace('#', 't'))) .expect(shouldContainInBody(parseError('#rue').replace(/#/g, 't')))
.end(done) .end(done)
}) })
}) })
@ -397,65 +400,59 @@ describe('express.json()', function () {
}) })
it('should error from verify', function (done) { it('should error from verify', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays') if (buf[0] === 0x5b) throw new Error('no arrays')
} }) }
request(app)
.post('/')
.set('Content-Type', 'application/json')
.send('["tobi"]')
.expect(403, 'no arrays', done)
}) })
it('should error with type = "entity.verify.failed"', function (done) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
} })
request(app) request(app)
.post('/') .post('/')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.set('X-Error-Property', 'type')
.send('["tobi"]') .send('["tobi"]')
.expect(403, 'entity.verify.failed', done) .expect(403, '[entity.verify.failed] no arrays', done)
}) })
it('should allow custom codes', function (done) { it('should allow custom codes', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x5b) return if (buf[0] !== 0x5b) return
var err = new Error('no arrays') var err = new Error('no arrays')
err.status = 400 err.status = 400
throw err throw err
} }) }
})
request(app) request(app)
.post('/') .post('/')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.send('["tobi"]') .send('["tobi"]')
.expect(400, 'no arrays', done) .expect(400, '[entity.verify.failed] no arrays', done)
}) })
it('should allow custom type', function (done) { it('should allow custom type', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x5b) return if (buf[0] !== 0x5b) return
var err = new Error('no arrays') var err = new Error('no arrays')
err.type = 'foo.bar' err.type = 'foo.bar'
throw err throw err
} }) }
})
request(app) request(app)
.post('/') .post('/')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.set('X-Error-Property', 'type')
.send('["tobi"]') .send('["tobi"]')
.expect(403, 'foo.bar', done) .expect(403, '[foo.bar] no arrays', done)
}) })
it('should include original body on error object', function (done) { it('should include original body on error object', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays') if (buf[0] === 0x5b) throw new Error('no arrays')
} }) }
})
request(app) request(app)
.post('/') .post('/')
@ -466,9 +463,11 @@ describe('express.json()', function () {
}) })
it('should allow pass-through', function (done) { it('should allow pass-through', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays') if (buf[0] === 0x5b) throw new Error('no arrays')
} }) }
})
request(app) request(app)
.post('/') .post('/')
@ -478,9 +477,11 @@ describe('express.json()', function () {
}) })
it('should work with different charsets', function (done) { it('should work with different charsets', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays') if (buf[0] === 0x5b) throw new Error('no arrays')
} }) }
})
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'application/json; charset=utf-16') test.set('Content-Type', 'application/json; charset=utf-16')
@ -489,14 +490,120 @@ describe('express.json()', function () {
}) })
it('should 415 on unknown charset prior to verify', function (done) { it('should 415 on unknown charset prior to verify', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
throw new Error('unexpected verify call') throw new Error('unexpected verify call')
} }) }
})
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'application/json; charset=x-bogus') test.set('Content-Type', 'application/json; charset=x-bogus')
test.write(Buffer.from('00000000', 'hex')) test.write(Buffer.from('00000000', 'hex'))
test.expect(415, 'unsupported charset "X-BOGUS"', done) test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done)
})
})
describeAsyncHooks('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.run(store, next)
})
app.use(express.json())
app.use(function (req, res, next) {
var local = req.asyncLocalStorage.getStore()
if (local) {
res.setHeader('x-store-foo', String(local.foo))
}
next()
})
app.use(function (err, req, res, next) {
var local = req.asyncLocalStorage.getStore()
if (local) {
res.setHeader('x-store-foo', String(local.foo))
}
res.status(err.status || 500)
res.send('[' + err.type + '] ' + err.message)
})
app.post('/', function (req, res) {
res.json(req.body)
})
this.app = app
})
it('should presist store', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'application/json')
.send('{"user":"tobi"}')
.expect(200)
.expect('x-store-foo', 'bar')
.expect('{"user":"tobi"}')
.end(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('')
.end(done)
})
it('should presist store when inflated', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/json')
test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex'))
test.expect(200)
test.expect('x-store-foo', 'bar')
test.expect('{"name":"论"}')
test.end(done)
})
it('should presist store when inflate error', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/json')
test.write(Buffer.from('1f8b080000000000000bab56cc4d55b2527ab16e97522d00515be1cc0e000000', 'hex'))
test.expect(400)
test.expect('x-store-foo', 'bar')
test.end(done)
})
it('should presist store when parse error', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'application/json')
.send('{"user":')
.expect(400)
.expect('x-store-foo', 'bar')
.end(done)
})
it('should presist store when limit exceeded', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'application/json')
.send('{"user":"' + Buffer.alloc(1024 * 100, '.').toString() + '"}')
.expect(413)
.expect('x-store-foo', 'bar')
.end(done)
}) })
}) })
@ -538,15 +645,7 @@ describe('express.json()', function () {
var test = request(this.app).post('/') var test = request(this.app).post('/')
test.set('Content-Type', 'application/json; charset=koi8-r') test.set('Content-Type', 'application/json; charset=koi8-r')
test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex')) test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex'))
test.expect(415, 'unsupported charset "KOI8-R"', done) test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done)
})
it('should error with type = "charset.unsupported"', function (done) {
var test = request(this.app).post('/')
test.set('Content-Type', 'application/json; charset=koi8-r')
test.set('X-Error-Property', 'type')
test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex'))
test.expect(415, 'charset.unsupported', done)
}) })
}) })
@ -599,16 +698,7 @@ describe('express.json()', function () {
test.set('Content-Encoding', 'nulls') test.set('Content-Encoding', 'nulls')
test.set('Content-Type', 'application/json') test.set('Content-Type', 'application/json')
test.write(Buffer.from('000000000000', 'hex')) test.write(Buffer.from('000000000000', 'hex'))
test.expect(415, 'unsupported content encoding "nulls"', done) test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done)
})
it('should error with type = "encoding.unsupported"', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'nulls')
test.set('Content-Type', 'application/json')
test.set('X-Error-Property', 'type')
test.write(Buffer.from('000000000000', 'hex'))
test.expect(415, 'encoding.unsupported', done)
}) })
it('should 400 on malformed encoding', function (done) { it('should 400 on malformed encoding', function (done) {
@ -638,8 +728,11 @@ function createApp (options) {
app.use(express.json(options)) app.use(express.json(options))
app.use(function (err, req, res, next) { app.use(function (err, req, res, next) {
// console.log(err)
res.status(err.status || 500) res.status(err.status || 500)
res.send(String(err[req.headers['x-error-property'] || 'message'])) res.send(String(req.headers['x-error-property']
? err[req.headers['x-error-property']]
: ('[' + err.type + '] ' + err.message)))
}) })
app.post('/', function (req, res) { app.post('/', function (req, res) {
@ -663,3 +756,11 @@ function shouldContainInBody (str) {
'expected \'' + res.text + '\' to contain \'' + str + '\'') 'expected \'' + res.text + '\' to contain \'' + str + '\'')
} }
} }
function tryRequire (name) {
try {
return require(name)
} catch (e) {
return {}
}
}

View File

@ -1,10 +1,15 @@
'use strict' 'use strict'
var assert = require('assert') var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer var Buffer = require('safe-buffer').Buffer
var express = require('..') var express = require('..')
var request = require('supertest') var request = require('supertest')
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('express.raw()', function () { describe('express.raw()', function () {
before(function () { before(function () {
this.app = createApp() this.app = createApp()
@ -102,6 +107,15 @@ describe('express.raw()', function () {
test.expect(413, done) test.expect(413, done)
}) })
it('should 413 when inflated body over limit', function (done) {
var app = createApp({ limit: '1kb' })
var test = request(app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/octet-stream')
test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a14704040000', 'hex'))
test.expect(413, done)
})
it('should accept number of bytes', function (done) { it('should accept number of bytes', function (done) {
var buf = Buffer.alloc(1028, '.') var buf = Buffer.alloc(1028, '.')
var app = createApp({ limit: 1024 }) var app = createApp({ limit: 1024 })
@ -134,6 +148,15 @@ describe('express.raw()', function () {
test.write(buf) test.write(buf)
test.expect(413, done) test.expect(413, done)
}) })
it('should not error when inflating', function (done) {
var app = createApp({ limit: '1kb' })
var test = request(app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/octet-stream')
test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a147040400', 'hex'))
test.expect(413, done)
})
}) })
describe('with inflate option', function () { describe('with inflate option', function () {
@ -147,7 +170,7 @@ describe('express.raw()', function () {
test.set('Content-Encoding', 'gzip') test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/octet-stream') test.set('Content-Type', 'application/octet-stream')
test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex'))
test.expect(415, 'content encoding unsupported', done) test.expect(415, '[encoding.unsupported] content encoding unsupported', done)
}) })
}) })
@ -263,34 +286,40 @@ describe('express.raw()', function () {
}) })
it('should error from verify', function (done) { it('should error from verify', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x00) throw new Error('no leading null') if (buf[0] === 0x00) throw new Error('no leading null')
} }) }
})
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'application/octet-stream') test.set('Content-Type', 'application/octet-stream')
test.write(Buffer.from('000102', 'hex')) test.write(Buffer.from('000102', 'hex'))
test.expect(403, 'no leading null', done) test.expect(403, '[entity.verify.failed] no leading null', done)
}) })
it('should allow custom codes', function (done) { it('should allow custom codes', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x00) return if (buf[0] !== 0x00) return
var err = new Error('no leading null') var err = new Error('no leading null')
err.status = 400 err.status = 400
throw err throw err
} }) }
})
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'application/octet-stream') test.set('Content-Type', 'application/octet-stream')
test.write(Buffer.from('000102', 'hex')) test.write(Buffer.from('000102', 'hex'))
test.expect(400, 'no leading null', done) test.expect(400, '[entity.verify.failed] no leading null', done)
}) })
it('should allow pass-through', function (done) { it('should allow pass-through', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x00) throw new Error('no leading null') if (buf[0] === 0x00) throw new Error('no leading null')
} }) }
})
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'application/octet-stream') test.set('Content-Type', 'application/octet-stream')
@ -299,6 +328,103 @@ describe('express.raw()', function () {
}) })
}) })
describeAsyncHooks('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.run(store, next)
})
app.use(express.raw())
app.use(function (req, res, next) {
var local = req.asyncLocalStorage.getStore()
if (local) {
res.setHeader('x-store-foo', String(local.foo))
}
next()
})
app.use(function (err, req, res, next) {
var local = req.asyncLocalStorage.getStore()
if (local) {
res.setHeader('x-store-foo', String(local.foo))
}
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)
}
})
this.app = app
})
it('should presist store', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'application/octet-stream')
.send('the user is tobi')
.expect(200)
.expect('x-store-foo', 'bar')
.expect({ buf: '746865207573657220697320746f6269' })
.end(done)
})
it('should presist 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')
.end(done)
})
it('should presist store when inflated', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/octet-stream')
test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex'))
test.expect(200)
test.expect('x-store-foo', 'bar')
test.expect({ buf: '6e616d653de8aeba' })
test.end(done)
})
it('should presist store when inflate error', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/octet-stream')
test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex'))
test.expect(400)
test.expect('x-store-foo', 'bar')
test.end(done)
})
it('should presist store when limit exceeded', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'application/octet-stream')
.send('the user is ' + Buffer.alloc(1024 * 100, '.').toString())
.expect(413)
.expect('x-store-foo', 'bar')
.end(done)
})
})
describe('charset', function () { describe('charset', function () {
before(function () { before(function () {
this.app = createApp() this.app = createApp()
@ -356,12 +482,12 @@ describe('express.raw()', function () {
test.expect(200, { buf: '6e616d653de8aeba' }, done) test.expect(200, { buf: '6e616d653de8aeba' }, done)
}) })
it('should fail on unknown encoding', function (done) { it('should 415 on unknown encoding', function (done) {
var test = request(this.app).post('/') var test = request(this.app).post('/')
test.set('Content-Encoding', 'nulls') test.set('Content-Encoding', 'nulls')
test.set('Content-Type', 'application/octet-stream') test.set('Content-Type', 'application/octet-stream')
test.write(Buffer.from('000000000000', 'hex')) test.write(Buffer.from('000000000000', 'hex'))
test.expect(415, 'unsupported content encoding "nulls"', done) test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done)
}) })
}) })
}) })
@ -373,7 +499,9 @@ function createApp (options) {
app.use(function (err, req, res, next) { app.use(function (err, req, res, next) {
res.status(err.status || 500) res.status(err.status || 500)
res.send(String(err[req.headers['x-error-property'] || 'message'])) res.send(String(req.headers['x-error-property']
? err[req.headers['x-error-property']]
: ('[' + err.type + '] ' + err.message)))
}) })
app.post('/', function (req, res) { app.post('/', function (req, res) {
@ -386,3 +514,11 @@ function createApp (options) {
return app return app
} }
function tryRequire (name) {
try {
return require(name)
} catch (e) {
return {}
}
}

View File

@ -1,10 +1,15 @@
'use strict' 'use strict'
var assert = require('assert') var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer var Buffer = require('safe-buffer').Buffer
var express = require('..') var express = require('..')
var request = require('supertest') var request = require('supertest')
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('express.text()', function () { describe('express.text()', function () {
before(function () { before(function () {
this.app = createApp() this.app = createApp()
@ -75,16 +80,16 @@ describe('express.text()', function () {
describe('with defaultCharset option', function () { describe('with defaultCharset option', function () {
it('should change default charset', function (done) { it('should change default charset', function (done) {
var app = createApp({ defaultCharset: 'koi8-r' }) var server = createApp({ defaultCharset: 'koi8-r' })
var test = request(app).post('/') var test = request(server).post('/')
test.set('Content-Type', 'text/plain') test.set('Content-Type', 'text/plain')
test.write(Buffer.from('6e616d6520697320cec5d4', 'hex')) test.write(Buffer.from('6e616d6520697320cec5d4', 'hex'))
test.expect(200, '"name is нет"', done) test.expect(200, '"name is нет"', done)
}) })
it('should honor content-type charset', function (done) { it('should honor content-type charset', function (done) {
var app = createApp({ defaultCharset: 'koi8-r' }) var server = createApp({ defaultCharset: 'koi8-r' })
var test = request(app).post('/') var test = request(server).post('/')
test.set('Content-Type', 'text/plain; charset=utf-8') test.set('Content-Type', 'text/plain; charset=utf-8')
test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) test.write(Buffer.from('6e616d6520697320e8aeba', 'hex'))
test.expect(200, '"name is 论"', done) test.expect(200, '"name is 论"', done)
@ -103,8 +108,8 @@ describe('express.text()', function () {
}) })
it('should 413 when over limit with chunked encoding', function (done) { it('should 413 when over limit with chunked encoding', function (done) {
var buf = Buffer.alloc(1028, '.')
var app = createApp({ limit: '1kb' }) var app = createApp({ limit: '1kb' })
var buf = Buffer.alloc(1028, '.')
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'text/plain') test.set('Content-Type', 'text/plain')
test.set('Transfer-Encoding', 'chunked') test.set('Transfer-Encoding', 'chunked')
@ -112,6 +117,15 @@ describe('express.text()', function () {
test.expect(413, done) test.expect(413, done)
}) })
it('should 413 when inflated body over limit', function (done) {
var app = createApp({ limit: '1kb' })
var test = request(app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'text/plain')
test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a14704040000', 'hex'))
test.expect(413, done)
})
it('should accept number of bytes', function (done) { it('should accept number of bytes', function (done) {
var buf = Buffer.alloc(1028, '.') var buf = Buffer.alloc(1028, '.')
request(createApp({ limit: 1024 })) request(createApp({ limit: 1024 }))
@ -136,8 +150,8 @@ describe('express.text()', function () {
}) })
it('should not hang response', function (done) { it('should not hang response', function (done) {
var buf = Buffer.alloc(10240, '.')
var app = createApp({ limit: '8kb' }) var app = createApp({ limit: '8kb' })
var buf = Buffer.alloc(10240, '.')
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'text/plain') test.set('Content-Type', 'text/plain')
test.write(buf) test.write(buf)
@ -145,6 +159,17 @@ describe('express.text()', function () {
test.write(buf) test.write(buf)
test.expect(413, done) test.expect(413, done)
}) })
it('should not error when inflating', function (done) {
var app = createApp({ limit: '1kb' })
var test = request(app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'text/plain')
test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a1470404', 'hex'))
setTimeout(function () {
test.expect(413, done)
}, 100)
})
}) })
describe('with inflate option', function () { describe('with inflate option', function () {
@ -158,7 +183,7 @@ describe('express.text()', function () {
test.set('Content-Encoding', 'gzip') test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'text/plain') test.set('Content-Type', 'text/plain')
test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex'))
test.expect(415, 'content encoding unsupported', done) test.expect(415, '[encoding.unsupported] content encoding unsupported', done)
}) })
}) })
@ -278,36 +303,42 @@ describe('express.text()', function () {
}) })
it('should error from verify', function (done) { it('should error from verify', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x20) throw new Error('no leading space') if (buf[0] === 0x20) throw new Error('no leading space')
} }) }
})
request(app) request(app)
.post('/') .post('/')
.set('Content-Type', 'text/plain') .set('Content-Type', 'text/plain')
.send(' user is tobi') .send(' user is tobi')
.expect(403, 'no leading space', done) .expect(403, '[entity.verify.failed] no leading space', done)
}) })
it('should allow custom codes', function (done) { it('should allow custom codes', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x20) return if (buf[0] !== 0x20) return
var err = new Error('no leading space') var err = new Error('no leading space')
err.status = 400 err.status = 400
throw err throw err
} }) }
})
request(app) request(app)
.post('/') .post('/')
.set('Content-Type', 'text/plain') .set('Content-Type', 'text/plain')
.send(' user is tobi') .send(' user is tobi')
.expect(400, 'no leading space', done) .expect(400, '[entity.verify.failed] no leading space', done)
}) })
it('should allow pass-through', function (done) { it('should allow pass-through', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x20) throw new Error('no leading space') if (buf[0] === 0x20) throw new Error('no leading space')
} }) }
})
request(app) request(app)
.post('/') .post('/')
@ -317,14 +348,109 @@ describe('express.text()', function () {
}) })
it('should 415 on unknown charset prior to verify', function (done) { it('should 415 on unknown charset prior to verify', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
throw new Error('unexpected verify call') throw new Error('unexpected verify call')
} }) }
})
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'text/plain; charset=x-bogus') test.set('Content-Type', 'text/plain; charset=x-bogus')
test.write(Buffer.from('00000000', 'hex')) test.write(Buffer.from('00000000', 'hex'))
test.expect(415, 'unsupported charset "X-BOGUS"', done) test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done)
})
})
describeAsyncHooks('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.run(store, next)
})
app.use(express.text())
app.use(function (req, res, next) {
var local = req.asyncLocalStorage.getStore()
if (local) {
res.setHeader('x-store-foo', String(local.foo))
}
next()
})
app.use(function (err, req, res, next) {
var local = req.asyncLocalStorage.getStore()
if (local) {
res.setHeader('x-store-foo', String(local.foo))
}
res.status(err.status || 500)
res.send('[' + err.type + '] ' + err.message)
})
app.post('/', function (req, res) {
res.json(req.body)
})
this.app = app
})
it('should presist store', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'text/plain')
.send('user is tobi')
.expect(200)
.expect('x-store-foo', 'bar')
.expect('"user is tobi"')
.end(done)
})
it('should presist 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')
.end(done)
})
it('should presist store when inflated', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'text/plain')
test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex'))
test.expect(200)
test.expect('x-store-foo', 'bar')
test.expect('"name is 论"')
test.end(done)
})
it('should presist store when inflate error', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'text/plain')
test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b0000', 'hex'))
test.expect(400)
test.expect('x-store-foo', 'bar')
test.end(done)
})
it('should presist store when limit exceeded', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'text/plain')
.send('user is ' + Buffer.alloc(1024 * 100, '.').toString())
.expect(413)
.expect('x-store-foo', 'bar')
.end(done)
}) })
}) })
@ -366,7 +492,7 @@ describe('express.text()', function () {
var test = request(this.app).post('/') var test = request(this.app).post('/')
test.set('Content-Type', 'text/plain; charset=x-bogus') test.set('Content-Type', 'text/plain; charset=x-bogus')
test.write(Buffer.from('00000000', 'hex')) test.write(Buffer.from('00000000', 'hex'))
test.expect(415, 'unsupported charset "X-BOGUS"', done) test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done)
}) })
}) })
@ -414,12 +540,12 @@ describe('express.text()', function () {
test.expect(200, '"name is 论"', done) test.expect(200, '"name is 论"', done)
}) })
it('should fail on unknown encoding', function (done) { it('should 415 on unknown encoding', function (done) {
var test = request(this.app).post('/') var test = request(this.app).post('/')
test.set('Content-Encoding', 'nulls') test.set('Content-Encoding', 'nulls')
test.set('Content-Type', 'text/plain') test.set('Content-Type', 'text/plain')
test.write(Buffer.from('000000000000', 'hex')) test.write(Buffer.from('000000000000', 'hex'))
test.expect(415, 'unsupported content encoding "nulls"', done) test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done)
}) })
}) })
}) })
@ -431,7 +557,9 @@ function createApp (options) {
app.use(function (err, req, res, next) { app.use(function (err, req, res, next) {
res.status(err.status || 500) res.status(err.status || 500)
res.send(err.message) res.send(String(req.headers['x-error-property']
? err[req.headers['x-error-property']]
: ('[' + err.type + '] ' + err.message)))
}) })
app.post('/', function (req, res) { app.post('/', function (req, res) {
@ -440,3 +568,11 @@ function createApp (options) {
return app return app
} }
function tryRequire (name) {
try {
return require(name)
} catch (e) {
return {}
}
}

View File

@ -1,10 +1,15 @@
'use strict' 'use strict'
var assert = require('assert') var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer var Buffer = require('safe-buffer').Buffer
var express = require('..') var express = require('..')
var request = require('supertest') var request = require('supertest')
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('express.urlencoded()', function () { describe('express.urlencoded()', function () {
before(function () { before(function () {
this.app = createApp() this.app = createApp()
@ -217,7 +222,7 @@ describe('express.urlencoded()', function () {
test.set('Content-Encoding', 'gzip') test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/x-www-form-urlencoded') test.set('Content-Type', 'application/x-www-form-urlencoded')
test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex'))
test.expect(415, 'content encoding unsupported', done) test.expect(415, '[encoding.unsupported] content encoding unsupported', done)
}) })
}) })
@ -248,8 +253,8 @@ describe('express.urlencoded()', function () {
}) })
it('should 413 when over limit with chunked encoding', function (done) { it('should 413 when over limit with chunked encoding', function (done) {
var buf = Buffer.alloc(1024, '.')
var app = createApp({ limit: '1kb' }) var app = createApp({ limit: '1kb' })
var buf = Buffer.alloc(1024, '.')
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'application/x-www-form-urlencoded') test.set('Content-Type', 'application/x-www-form-urlencoded')
test.set('Transfer-Encoding', 'chunked') test.set('Transfer-Encoding', 'chunked')
@ -258,6 +263,15 @@ describe('express.urlencoded()', function () {
test.expect(413, done) test.expect(413, done)
}) })
it('should 413 when inflated body over limit', function (done) {
var app = createApp({ limit: '1kb' })
var test = request(app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/x-www-form-urlencoded')
test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f9204040000', 'hex'))
test.expect(413, done)
})
it('should accept number of bytes', function (done) { it('should accept number of bytes', function (done) {
var buf = Buffer.alloc(1024, '.') var buf = Buffer.alloc(1024, '.')
request(createApp({ limit: 1024 })) request(createApp({ limit: 1024 }))
@ -282,8 +296,8 @@ describe('express.urlencoded()', function () {
}) })
it('should not hang response', function (done) { it('should not hang response', function (done) {
var buf = Buffer.alloc(10240, '.')
var app = createApp({ limit: '8kb' }) var app = createApp({ limit: '8kb' })
var buf = Buffer.alloc(10240, '.')
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'application/x-www-form-urlencoded') test.set('Content-Type', 'application/x-www-form-urlencoded')
test.write(buf) test.write(buf)
@ -291,6 +305,15 @@ describe('express.urlencoded()', function () {
test.write(buf) test.write(buf)
test.expect(413, done) test.expect(413, done)
}) })
it('should not error when inflating', function (done) {
var app = createApp({ limit: '1kb' })
var test = request(app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/x-www-form-urlencoded')
test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f92040400', 'hex'))
test.expect(413, done)
})
}) })
describe('with parameterLimit option', function () { describe('with parameterLimit option', function () {
@ -310,16 +333,7 @@ describe('express.urlencoded()', function () {
.post('/') .post('/')
.set('Content-Type', 'application/x-www-form-urlencoded') .set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(11)) .send(createManyParams(11))
.expect(413, /too many parameters/, done) .expect(413, '[parameters.too.many] too many parameters', done)
})
it('should error with type = "parameters.too.many"', function (done) {
request(createApp({ extended: false, parameterLimit: 10 }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.set('X-Error-Property', 'type')
.send(createManyParams(11))
.expect(413, 'parameters.too.many', done)
}) })
it('should work when at the limit', function (done) { it('should work when at the limit', function (done) {
@ -374,16 +388,7 @@ describe('express.urlencoded()', function () {
.post('/') .post('/')
.set('Content-Type', 'application/x-www-form-urlencoded') .set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(11)) .send(createManyParams(11))
.expect(413, /too many parameters/, done) .expect(413, '[parameters.too.many] too many parameters', done)
})
it('should error with type = "parameters.too.many"', function (done) {
request(createApp({ extended: true, parameterLimit: 10 }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.set('X-Error-Property', 'type')
.send(createManyParams(11))
.expect(413, 'parameters.too.many', done)
}) })
it('should work when at the limit', function (done) { it('should work when at the limit', function (done) {
@ -526,65 +531,59 @@ describe('express.urlencoded()', function () {
}) })
it('should error from verify', function (done) { it('should error from verify', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x20) throw new Error('no leading space') if (buf[0] === 0x20) throw new Error('no leading space')
} }) }
request(app)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(' user=tobi')
.expect(403, 'no leading space', done)
}) })
it('should error with type = "entity.verify.failed"', function (done) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x20) throw new Error('no leading space')
} })
request(app) request(app)
.post('/') .post('/')
.set('Content-Type', 'application/x-www-form-urlencoded') .set('Content-Type', 'application/x-www-form-urlencoded')
.set('X-Error-Property', 'type')
.send(' user=tobi') .send(' user=tobi')
.expect(403, 'entity.verify.failed', done) .expect(403, '[entity.verify.failed] no leading space', done)
}) })
it('should allow custom codes', function (done) { it('should allow custom codes', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x20) return if (buf[0] !== 0x20) return
var err = new Error('no leading space') var err = new Error('no leading space')
err.status = 400 err.status = 400
throw err throw err
} }) }
})
request(app) request(app)
.post('/') .post('/')
.set('Content-Type', 'application/x-www-form-urlencoded') .set('Content-Type', 'application/x-www-form-urlencoded')
.send(' user=tobi') .send(' user=tobi')
.expect(400, 'no leading space', done) .expect(400, '[entity.verify.failed] no leading space', done)
}) })
it('should allow custom type', function (done) { it('should allow custom type', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x20) return if (buf[0] !== 0x20) return
var err = new Error('no leading space') var err = new Error('no leading space')
err.type = 'foo.bar' err.type = 'foo.bar'
throw err throw err
} }) }
})
request(app) request(app)
.post('/') .post('/')
.set('Content-Type', 'application/x-www-form-urlencoded') .set('Content-Type', 'application/x-www-form-urlencoded')
.set('X-Error-Property', 'type')
.send(' user=tobi') .send(' user=tobi')
.expect(403, 'foo.bar', done) .expect(403, '[foo.bar] no leading space', done)
}) })
it('should allow pass-through', function (done) { it('should allow pass-through', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays') if (buf[0] === 0x5b) throw new Error('no arrays')
} }) }
})
request(app) request(app)
.post('/') .post('/')
@ -594,14 +593,109 @@ describe('express.urlencoded()', function () {
}) })
it('should 415 on unknown charset prior to verify', function (done) { it('should 415 on unknown charset prior to verify', function (done) {
var app = createApp({ verify: function (req, res, buf) { var app = createApp({
verify: function (req, res, buf) {
throw new Error('unexpected verify call') throw new Error('unexpected verify call')
} }) }
})
var test = request(app).post('/') var test = request(app).post('/')
test.set('Content-Type', 'application/x-www-form-urlencoded; charset=x-bogus') test.set('Content-Type', 'application/x-www-form-urlencoded; charset=x-bogus')
test.write(Buffer.from('00000000', 'hex')) test.write(Buffer.from('00000000', 'hex'))
test.expect(415, 'unsupported charset "X-BOGUS"', done) test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done)
})
})
describeAsyncHooks('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.run(store, next)
})
app.use(express.urlencoded())
app.use(function (req, res, next) {
var local = req.asyncLocalStorage.getStore()
if (local) {
res.setHeader('x-store-foo', String(local.foo))
}
next()
})
app.use(function (err, req, res, next) {
var local = req.asyncLocalStorage.getStore()
if (local) {
res.setHeader('x-store-foo', String(local.foo))
}
res.status(err.status || 500)
res.send('[' + err.type + '] ' + err.message)
})
app.post('/', function (req, res) {
res.json(req.body)
})
this.app = app
})
it('should presist store', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send('user=tobi')
.expect(200)
.expect('x-store-foo', 'bar')
.expect('{"user":"tobi"}')
.end(done)
})
it('should presist 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')
.end(done)
})
it('should presist store when inflated', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/x-www-form-urlencoded')
test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex'))
test.expect(200)
test.expect('x-store-foo', 'bar')
test.expect('{"name":"论"}')
test.end(done)
})
it('should presist store when inflate error', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/x-www-form-urlencoded')
test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex'))
test.expect(400)
test.expect('x-store-foo', 'bar')
test.end(done)
})
it('should presist store when limit exceeded', function (done) {
request(this.app)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send('user=' + Buffer.alloc(1024 * 100, '.').toString())
.expect(413)
.expect('x-store-foo', 'bar')
.end(done)
}) })
}) })
@ -636,7 +730,7 @@ describe('express.urlencoded()', function () {
var test = request(this.app).post('/') var test = request(this.app).post('/')
test.set('Content-Type', 'application/x-www-form-urlencoded; charset=koi8-r') test.set('Content-Type', 'application/x-www-form-urlencoded; charset=koi8-r')
test.write(Buffer.from('6e616d653dcec5d4', 'hex')) test.write(Buffer.from('6e616d653dcec5d4', 'hex'))
test.expect(415, 'unsupported charset "KOI8-R"', done) test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done)
}) })
}) })
@ -684,12 +778,12 @@ describe('express.urlencoded()', function () {
test.expect(200, '{"name":"论"}', done) test.expect(200, '{"name":"论"}', done)
}) })
it('should fail on unknown encoding', function (done) { it('should 415 on unknown encoding', function (done) {
var test = request(this.app).post('/') var test = request(this.app).post('/')
test.set('Content-Encoding', 'nulls') test.set('Content-Encoding', 'nulls')
test.set('Content-Type', 'application/x-www-form-urlencoded') test.set('Content-Type', 'application/x-www-form-urlencoded')
test.write(Buffer.from('000000000000', 'hex')) test.write(Buffer.from('000000000000', 'hex'))
test.expect(415, 'unsupported content encoding "nulls"', done) test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done)
}) })
}) })
}) })
@ -718,7 +812,9 @@ function createApp (options) {
app.use(function (err, req, res, next) { app.use(function (err, req, res, next) {
res.status(err.status || 500) res.status(err.status || 500)
res.send(String(err[req.headers['x-error-property'] || 'message'])) res.send(String(req.headers['x-error-property']
? err[req.headers['x-error-property']]
: ('[' + err.type + '] ' + err.message)))
}) })
app.post('/', function (req, res) { app.post('/', function (req, res) {
@ -733,3 +829,11 @@ function expectKeyCount (count) {
assert.strictEqual(Object.keys(JSON.parse(res.text)).length, count) assert.strictEqual(Object.keys(JSON.parse(res.text)).length, count)
} }
} }
function tryRequire (name) {
try {
return require(name)
} catch (e) {
return {}
}
}

View File

@ -1,2 +0,0 @@
--require should
--slow 20

View File

@ -1,31 +1,29 @@
'use strict' 'use strict'
var assert = require('assert')
var express = require('..') var express = require('..')
var request = require('supertest') var request = require('supertest')
var should = require('should')
describe('res', function () { describe('res', function () {
// note about these tests: "Link" and "X-*" are chosen because
// the common node.js versions white list which _incoming_
// headers can appear multiple times; there is no such white list
// for outgoing, though
describe('.append(field, val)', function () { describe('.append(field, val)', function () {
it('should append multiple headers', function (done) { it('should append multiple headers', function (done) {
var app = express() var app = express()
app.use(function (req, res, next) { app.use(function (req, res, next) {
res.append('Link', '<http://localhost/>') res.append('Set-Cookie', 'foo=bar')
next() next()
}) })
app.use(function (req, res) { app.use(function (req, res) {
res.append('Link', '<http://localhost:80/>') res.append('Set-Cookie', 'fizz=buzz')
res.end() res.end()
}) })
request(app) request(app)
.get('/') .get('/')
.expect('Link', '<http://localhost/>, <http://localhost:80/>', done) .expect(200)
.expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz']))
.end(done)
}) })
it('should accept array of values', function (done) { it('should accept array of values', function (done) {
@ -38,50 +36,53 @@ describe('res', function () {
request(app) request(app)
.get('/') .get('/')
.expect(function (res) { .expect(200)
should(res.headers['set-cookie']).eql(['foo=bar', 'fizz=buzz']) .expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz']))
}) .end(done)
.expect(200, done)
}) })
it('should get reset by res.set(field, val)', function (done) { it('should get reset by res.set(field, val)', function (done) {
var app = express() var app = express()
app.use(function (req, res, next) { app.use(function (req, res, next) {
res.append('Link', '<http://localhost/>') res.append('Set-Cookie', 'foo=bar')
res.append('Link', '<http://localhost:80/>') res.append('Set-Cookie', 'fizz=buzz')
next() next()
}) })
app.use(function (req, res) { app.use(function (req, res) {
res.set('Link', '<http://127.0.0.1/>') res.set('Set-Cookie', 'pet=tobi')
res.end() res.end()
}); });
request(app) request(app)
.get('/') .get('/')
.expect('Link', '<http://127.0.0.1/>', done) .expect(200)
.expect(shouldHaveHeaderValues('Set-Cookie', ['pet=tobi']))
.end(done)
}) })
it('should work with res.set(field, val) first', function (done) { it('should work with res.set(field, val) first', function (done) {
var app = express() var app = express()
app.use(function (req, res, next) { app.use(function (req, res, next) {
res.set('Link', '<http://localhost/>') res.set('Set-Cookie', 'foo=bar')
next() next()
}) })
app.use(function(req, res){ app.use(function(req, res){
res.append('Link', '<http://localhost:80/>') res.append('Set-Cookie', 'fizz=buzz')
res.end() res.end()
}) })
request(app) request(app)
.get('/') .get('/')
.expect('Link', '<http://localhost/>, <http://localhost:80/>', done) .expect(200)
.expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz']))
.end(done)
}) })
it('should work with cookies', function (done) { it('should work together with res.cookie', function (done) {
var app = express() var app = express()
app.use(function (req, res, next) { app.use(function (req, res, next) {
@ -90,16 +91,26 @@ describe('res', function () {
}) })
app.use(function (req, res) { app.use(function (req, res) {
res.append('Set-Cookie', 'bar=baz') res.append('Set-Cookie', 'fizz=buzz')
res.end() res.end()
}) })
request(app) request(app)
.get('/') .get('/')
.expect(function (res) { .expect(200)
should(res.headers['set-cookie']).eql(['foo=bar; Path=/', 'bar=baz']) .expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar; Path=/', 'fizz=buzz']))
}) .end(done)
.expect(200, done)
}) })
}) })
}) })
function shouldHaveHeaderValues (key, values) {
return function (res) {
var headers = res.headers[key.toLowerCase()]
assert.ok(headers, 'should have header "' + key + '"')
assert.strictEqual(headers.length, values.length, 'should have ' + values.length + ' occurances of "' + key + '"')
for (var i = 0; i < values.length; i++) {
assert.strictEqual(headers[i], values[i])
}
}
}

View File

@ -67,6 +67,37 @@ describe('res', function(){
.expect(200, done) .expect(200, done)
}) })
describe('expires', function () {
it('should throw on invalid date', function (done) {
var app = express()
app.use(function (req, res) {
res.cookie('name', 'tobi', { expires: new Date(NaN) })
res.end()
})
request(app)
.get('/')
.expect(500, /option expires is invalid/, done)
})
})
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(){ describe('maxAge', function(){
it('should set relative expires', function(done){ it('should set relative expires', function(done){
var app = express(); var app = express();
@ -111,6 +142,36 @@ describe('res', function(){
.expect(200, optionsCopy, done) .expect(200, optionsCopy, done)
}) })
it('should not throw on null', function (done) {
var app = express()
app.use(function (req, res) {
res.cookie('name', 'tobi', { maxAge: null })
res.end()
})
request(app)
.get('/')
.expect(200)
.expect('Set-Cookie', 'name=tobi; Path=/')
.end(done)
})
it('should not throw on undefined', function (done) {
var app = express()
app.use(function (req, res) {
res.cookie('name', 'tobi', { maxAge: undefined })
res.end()
})
request(app)
.get('/')
.expect(200)
.expect('Set-Cookie', 'name=tobi; Path=/')
.end(done)
})
it('should throw an error with invalid maxAge', function (done) { it('should throw an error with invalid maxAge', function (done) {
var app = express() var app = express()
@ -125,6 +186,63 @@ describe('res', function(){
}) })
}) })
describe('priority', function () {
it('should set low priority', function (done) {
var app = express()
app.use(function (req, res) {
res.cookie('name', 'tobi', { priority: 'low' })
res.end()
})
request(app)
.get('/')
.expect('Set-Cookie', /Priority=Low/)
.expect(200, done)
})
it('should set medium priority', function (done) {
var app = express()
app.use(function (req, res) {
res.cookie('name', 'tobi', { priority: 'medium' })
res.end()
})
request(app)
.get('/')
.expect('Set-Cookie', /Priority=Medium/)
.expect(200, done)
})
it('should set high priority', function (done) {
var app = express()
app.use(function (req, res) {
res.cookie('name', 'tobi', { priority: 'high' })
res.end()
})
request(app)
.get('/')
.expect('Set-Cookie', /Priority=High/)
.expect(200, done)
})
it('should throw with invalid priority', function (done) {
var app = express()
app.use(function (req, res) {
res.cookie('name', 'tobi', { priority: 'foobar' })
res.end()
})
request(app)
.get('/')
.expect(500, /option priority is invalid/, done)
})
})
describe('signed', function(){ describe('signed', function(){
it('should generate a signed JSON cookie', function(done){ it('should generate a signed JSON cookie', function(done){
var app = express(); var app = express();

View File

@ -1,10 +1,19 @@
'use strict' 'use strict'
var after = require('after'); var after = require('after');
var assert = require('assert'); var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer var Buffer = require('safe-buffer').Buffer
var express = require('..'); var express = require('..');
var path = require('path')
var request = require('supertest'); 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('res', function(){
describe('.download(path)', function(){ describe('.download(path)', function(){
@ -81,6 +90,272 @@ describe('res', function(){
.expect('Content-Disposition', 'attachment; filename="user.html"') .expect('Content-Disposition', 'attachment; filename="user.html"')
.expect(200, cb); .expect(200, cb);
}) })
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.download('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('Content-Disposition', 'attachment; filename="name.txt"')
.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.download('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('.download(path, options)', function () {
it('should allow options to res.sendFile()', function (done) {
var app = express()
app.use(function (req, res) {
res.download('test/fixtures/.name', {
dotfiles: 'allow',
maxAge: '4h'
})
})
request(app)
.get('/')
.expect(200)
.expect('Content-Disposition', 'attachment; filename=".name"')
.expect('Cache-Control', 'public, max-age=14400')
.expect(utils.shouldHaveBody(Buffer.from('tobi')))
.end(done)
})
describe('with "headers" option', function () {
it('should set headers on response', function (done) {
var app = express()
app.use(function (req, res) {
res.download('test/fixtures/user.html', {
headers: {
'X-Foo': 'Bar',
'X-Bar': 'Foo'
}
})
})
request(app)
.get('/')
.expect(200)
.expect('X-Foo', 'Bar')
.expect('X-Bar', 'Foo')
.end(done)
})
it('should use last header when duplicated', function (done) {
var app = express()
app.use(function (req, res) {
res.download('test/fixtures/user.html', {
headers: {
'X-Foo': 'Bar',
'x-foo': 'bar'
}
})
})
request(app)
.get('/')
.expect(200)
.expect('X-Foo', 'bar')
.end(done)
})
it('should override Content-Type', function (done) {
var app = express()
app.use(function (req, res) {
res.download('test/fixtures/user.html', {
headers: {
'Content-Type': 'text/x-custom'
}
})
})
request(app)
.get('/')
.expect(200)
.expect('Content-Type', 'text/x-custom')
.end(done)
})
it('should not set headers on 404', function (done) {
var app = express()
app.use(function (req, res) {
res.download('test/fixtures/does-not-exist', {
headers: {
'X-Foo': 'Bar'
}
})
})
request(app)
.get('/')
.expect(404)
.expect(utils.shouldNotHaveHeader('X-Foo'))
.end(done)
})
describe('when headers contains Content-Disposition', function () {
it('should be ignored', function (done) {
var app = express()
app.use(function (req, res) {
res.download('test/fixtures/user.html', {
headers: {
'Content-Disposition': 'inline'
}
})
})
request(app)
.get('/')
.expect(200)
.expect('Content-Disposition', 'attachment; filename="user.html"')
.end(done)
})
it('should be ignored case-insensitively', function (done) {
var app = express()
app.use(function (req, res) {
res.download('test/fixtures/user.html', {
headers: {
'content-disposition': 'inline'
}
})
})
request(app)
.get('/')
.expect(200)
.expect('Content-Disposition', 'attachment; filename="user.html"')
.end(done)
})
})
})
describe('with "root" option', function () {
it('should allow relative path', function (done) {
var app = express()
app.use(function (req, res) {
res.download('name.txt', {
root: FIXTURES_PATH
})
})
request(app)
.get('/')
.expect(200)
.expect('Content-Disposition', 'attachment; filename="name.txt"')
.expect(utils.shouldHaveBody(Buffer.from('tobi')))
.end(done)
})
it('should allow up within root', function (done) {
var app = express()
app.use(function (req, res) {
res.download('fake/../name.txt', {
root: FIXTURES_PATH
})
})
request(app)
.get('/')
.expect(200)
.expect('Content-Disposition', 'attachment; filename="name.txt"')
.expect(utils.shouldHaveBody(Buffer.from('tobi')))
.end(done)
})
it('should reject up outside root', function (done) {
var app = express()
app.use(function (req, res) {
var p = '..' + path.sep +
path.relative(path.dirname(FIXTURES_PATH), path.join(FIXTURES_PATH, 'name.txt'))
res.download(p, {
root: FIXTURES_PATH
})
})
request(app)
.get('/')
.expect(403)
.expect(utils.shouldNotHaveHeader('Content-Disposition'))
.end(done)
})
it('should reject reading outside root', function (done) {
var app = express()
app.use(function (req, res) {
res.download('../name.txt', {
root: FIXTURES_PATH
})
})
request(app)
.get('/')
.expect(403)
.expect(utils.shouldNotHaveHeader('Content-Disposition'))
.end(done)
})
})
}) })
describe('.download(path, filename, fn)', function(){ describe('.download(path, filename, fn)', function(){
@ -107,7 +382,7 @@ describe('res', function(){
var options = {} var options = {}
app.use(function (req, res) { app.use(function (req, res) {
res.download('test/fixtures/user.html', 'document', options, done) res.download('test/fixtures/user.html', 'document', options, cb)
}) })
request(app) request(app)
@ -133,7 +408,7 @@ describe('res', function(){
.expect(200) .expect(200)
.expect('Content-Disposition', 'attachment; filename="document"') .expect('Content-Disposition', 'attachment; filename="document"')
.expect('Cache-Control', 'public, max-age=14400') .expect('Cache-Control', 'public, max-age=14400')
.expect(shouldHaveBody(Buffer.from('tobi'))) .expect(utils.shouldHaveBody(Buffer.from('tobi')))
.end(done) .end(done)
}) })
@ -208,24 +483,16 @@ describe('res', function(){
request(app) request(app)
.get('/') .get('/')
.expect(shouldNotHaveHeader('Content-Disposition')) .expect(utils.shouldNotHaveHeader('Content-Disposition'))
.expect(200, 'failed', done); .expect(200, 'failed', done)
}) })
}) })
}) })
function shouldHaveBody (buf) { function tryRequire (name) {
return function (res) { try {
var body = !Buffer.isBuffer(res.body) return require(name)
? Buffer.from(res.text) } catch (e) {
: res.body return {}
assert.ok(body, 'response has body')
assert.strictEqual(body.toString('hex'), buf.toString('hex'))
} }
} }
function shouldNotHaveHeader(header) {
return function (res) {
assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header);
};
}

View File

@ -52,13 +52,18 @@ var app3 = express();
app3.use(function(req, res, next){ app3.use(function(req, res, next){
res.format({ res.format({
text: function(){ res.send('hey') }, text: function(){ res.send('hey') },
default: function(){ res.send('default') } default: function (a, b, c) {
assert(req === a)
assert(res === b)
assert(next === c)
res.send('default')
}
}) })
}); });
var app4 = express(); var app4 = express();
app4.get('/', function(req, res, next){ app4.get('/', function (req, res) {
res.format({ res.format({
text: function(){ res.send('hey') }, text: function(){ res.send('hey') },
html: function(){ res.send('<p>hey</p>') }, html: function(){ res.send('<p>hey</p>') },
@ -122,6 +127,28 @@ describe('res', function(){
.set('Accept', '*/*') .set('Accept', '*/*')
.expect('hey', done); .expect('hey', done);
}) })
it('should be able to invoke other formatter', function (done) {
var app = express()
app.use(function (req, res, next) {
res.format({
json: function () { res.send('json') },
default: function () {
res.header('x-default', '1')
this.json()
}
})
})
request(app)
.get('/')
.set('Accept', 'text/plain')
.expect(200)
.expect('x-default', '1')
.expect('json')
.end(done)
})
}) })
describe('in router', function(){ describe('in router', function(){
@ -132,7 +159,7 @@ describe('res', function(){
var app = express(); var app = express();
var router = express.Router(); var router = express.Router();
router.get('/', function(req, res, next){ router.get('/', function (req, res) {
res.format({ res.format({
text: function(){ res.send('hey') }, text: function(){ res.send('hey') },
html: function(){ res.send('<p>hey</p>') }, html: function(){ res.send('<p>hey</p>') },

View File

@ -1,13 +1,27 @@
'use strict' 'use strict'
var express = require('../') var express = require('../')
, request = require('supertest'); , request = require('supertest')
, url = require('url');
describe('res', function(){ describe('res', function(){
describe('.location(url)', function(){ describe('.location(url)', function(){
it('should set the header', function(done){ it('should set the header', function(done){
var app = express(); var app = express();
app.use(function(req, res){
res.location('http://google.com/').end();
});
request(app)
.get('/')
.expect('Location', 'http://google.com/')
.expect(200, done)
})
it('should preserve trailing slashes when not present', function(done){
var app = express();
app.use(function(req, res){ app.use(function(req, res){
res.location('http://google.com').end(); res.location('http://google.com').end();
}); });
@ -31,6 +45,36 @@ describe('res', function(){
.expect(200, done) .expect(200, done)
}) })
it('should not encode bad "url"', function (done) {
var app = express()
app.use(function (req, res) {
// This is here to show a basic check one might do which
// would pass but then the location header would still be bad
if (url.parse(req.query.q).host !== 'google.com') {
res.status(400).end('Bad url');
}
res.location(req.query.q).end();
});
request(app)
.get('/?q=http://google.com\\@apple.com')
.expect(200)
.expect('Location', 'http://google.com\\@apple.com')
.end(function (err) {
if (err) {
throw err;
}
// This ensures that our protocol check is case insensitive
request(app)
.get('/?q=HTTP://google.com\\@apple.com')
.expect(200)
.expect('Location', 'HTTP://google.com\\@apple.com')
.end(done)
});
});
it('should not touch already-encoded sequences in "url"', function (done) { it('should not touch already-encoded sequences in "url"', function (done) {
var app = express() var app = express()

View File

@ -1,6 +1,5 @@
'use strict' 'use strict'
var assert = require('assert')
var express = require('..'); var express = require('..');
var request = require('supertest'); var request = require('supertest');
var utils = require('./support/utils'); var utils = require('./support/utils');
@ -74,7 +73,7 @@ describe('res', function(){
.head('/') .head('/')
.expect(302) .expect(302)
.expect('Location', 'http://google.com') .expect('Location', 'http://google.com')
.expect(shouldNotHaveBody()) .expect(utils.shouldNotHaveBody())
.end(done) .end(done)
}) })
}) })
@ -190,14 +189,8 @@ describe('res', function(){
.expect('location', 'http://google.com') .expect('location', 'http://google.com')
.expect('content-length', '0') .expect('content-length', '0')
.expect(utils.shouldNotHaveHeader('Content-Type')) .expect(utils.shouldNotHaveHeader('Content-Type'))
.expect(shouldNotHaveBody()) .expect(utils.shouldNotHaveBody())
.end(done) .end(done)
}) })
}) })
}) })
function shouldNotHaveBody () {
return function (res) {
assert.ok(res.text === '' || res.text === undefined)
}
}

View File

@ -146,7 +146,7 @@ describe('res', function(){
.get('/') .get('/')
.expect(200) .expect(200)
.expect('Content-Type', 'application/octet-stream') .expect('Content-Type', 'application/octet-stream')
.expect(shouldHaveBody(Buffer.from('hello'))) .expect(utils.shouldHaveBody(Buffer.from('hello')))
.end(done) .end(done)
}) })
@ -216,7 +216,7 @@ describe('res', function(){
request(app) request(app)
.head('/') .head('/')
.expect(200) .expect(200)
.expect(shouldNotHaveBody()) .expect(utils.shouldNotHaveBody())
.end(done) .end(done)
}) })
}) })
@ -238,6 +238,22 @@ describe('res', function(){
}) })
}) })
describe('when .statusCode is 205', function () {
it('should strip Transfer-Encoding field and body, set Content-Length', function (done) {
var app = express()
app.use(function (req, res) {
res.status(205).set('Transfer-Encoding', 'chunked').send('foo')
})
request(app)
.get('/')
.expect(utils.shouldNotHaveHeader('Transfer-Encoding'))
.expect('Content-Length', '0')
.expect(205, '', done)
})
})
describe('when .statusCode is 304', function(){ describe('when .statusCode is 304', function(){
it('should strip Content-* fields, Transfer-Encoding field, and body', function(done){ it('should strip Content-* fields, Transfer-Encoding field, and body', function(done){
var app = express(); var app = express();
@ -533,19 +549,3 @@ describe('res', function(){
}) })
}) })
}) })
function shouldHaveBody (buf) {
return function (res) {
var body = !Buffer.isBuffer(res.body)
? Buffer.from(res.text)
: res.body
assert.ok(body, 'response has body')
assert.strictEqual(body.toString('hex'), buf.toString('hex'))
}
}
function shouldNotHaveBody () {
return function (res) {
assert.ok(res.text === '' || res.text === undefined)
}
}

View File

@ -2,15 +2,19 @@
var after = require('after'); var after = require('after');
var assert = require('assert') var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer var Buffer = require('safe-buffer').Buffer
var express = require('../') var express = require('../')
, request = require('supertest') , request = require('supertest')
var onFinished = require('on-finished'); var onFinished = require('on-finished');
var path = require('path'); var path = require('path');
var should = require('should');
var fixtures = path.join(__dirname, 'fixtures'); var fixtures = path.join(__dirname, 'fixtures');
var utils = require('./support/utils'); var utils = require('./support/utils');
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('res', function(){ describe('res', function(){
describe('.sendFile(path)', function () { describe('.sendFile(path)', function () {
it('should error missing path', function (done) { it('should error missing path', function (done) {
@ -29,6 +33,14 @@ describe('res', function(){
.expect(500, /TypeError: path must be a string to res.sendFile/, done) .expect(500, /TypeError: path must be a string to res.sendFile/, done)
}) })
it('should error for non-absolute path', function (done) {
var app = createApp('name.txt')
request(app)
.get('/')
.expect(500, /TypeError: path must be absolute/, done)
})
it('should transfer a file', function (done) { it('should transfer a file', function (done) {
var app = createApp(path.resolve(fixtures, 'name.txt')); var app = createApp(path.resolve(fixtures, 'name.txt'));
@ -91,6 +103,23 @@ describe('res', function(){
.expect(404, done); .expect(404, done);
}); });
it('should send cache-control by default', function (done) {
var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'))
request(app)
.get('/')
.expect('Cache-Control', 'public, max-age=0')
.expect(200, done)
})
it('should not serve dotfiles by default', function (done) {
var app = createApp(path.resolve(__dirname, 'fixtures/.name'))
request(app)
.get('/')
.expect(404, done)
})
it('should not override manual content-types', function (done) { it('should not override manual content-types', function (done) {
var app = express(); var app = express();
@ -132,136 +161,6 @@ describe('res', function(){
server.close(cb) server.close(cb)
}) })
}) })
describe('with "cacheControl" option', function () {
it('should enable cacheControl by default', function (done) {
var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'))
request(app)
.get('/')
.expect('Cache-Control', 'public, max-age=0')
.expect(200, done)
})
it('should accept cacheControl option', function (done) {
var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), { cacheControl: false })
request(app)
.get('/')
.expect(utils.shouldNotHaveHeader('Cache-Control'))
.expect(200, done)
})
})
describe('with "dotfiles" option', function () {
it('should not serve dotfiles by default', function (done) {
var app = createApp(path.resolve(__dirname, 'fixtures/.name'));
request(app)
.get('/')
.expect(404, done);
});
it('should accept dotfiles option', function(done){
var app = createApp(path.resolve(__dirname, 'fixtures/.name'), { dotfiles: 'allow' });
request(app)
.get('/')
.expect(200)
.expect(shouldHaveBody(Buffer.from('tobi')))
.end(done)
});
});
describe('with "headers" option', function () {
it('should accept headers option', function (done) {
var headers = {
'x-success': 'sent',
'x-other': 'done'
};
var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), { 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 headers = { 'x-success': 'sent' };
var app = createApp(path.resolve(__dirname, 'fixtures/does-not-exist'), { headers: headers });
request(app)
.get('/')
.expect(utils.shouldNotHaveHeader('X-Success'))
.expect(404, done);
});
});
describe('with "immutable" option', function () {
it('should add immutable cache-control directive', function (done) {
var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), {
immutable: true,
maxAge: '4h'
})
request(app)
.get('/')
.expect('Cache-Control', 'public, max-age=14400, immutable')
.expect(200, done)
})
})
describe('with "maxAge" option', function () {
it('should set cache-control max-age from number', function (done) {
var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), {
maxAge: 14400000
})
request(app)
.get('/')
.expect('Cache-Control', 'public, max-age=14400')
.expect(200, done)
})
it('should set cache-control max-age from string', function (done) {
var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), {
maxAge: '4h'
})
request(app)
.get('/')
.expect('Cache-Control', 'public, max-age=14400')
.expect(200, done)
})
})
describe('with "root" option', function () {
it('should not transfer relative with without', function (done) {
var app = createApp('test/fixtures/name.txt');
request(app)
.get('/')
.expect(500, /must be absolute/, done);
})
it('should serve relative to "root"', function (done) {
var app = createApp('name.txt', {root: fixtures});
request(app)
.get('/')
.expect(200, 'tobi', done);
})
it('should disallow requesting out of "root"', function (done) {
var app = createApp('foo/../../user.html', {root: fixtures});
request(app)
.get('/')
.expect(403, done);
})
})
}) })
describe('.sendFile(path, fn)', function () { describe('.sendFile(path, fn)', function () {
@ -359,15 +258,71 @@ describe('res', function(){
app.use(function (req, res) { app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) { res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) {
should(err).be.ok() res.send(err ? 'got ' + err.status + ' error' : 'no error')
err.status.should.equal(404);
res.send('got it');
}); });
}); });
request(app) request(app)
.get('/') .get('/')
.expect(200, 'got it', done); .expect(200, 'got 404 error', done)
})
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(path.resolve(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(path.resolve(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)
})
}) })
}) })
@ -377,6 +332,563 @@ describe('res', function(){
.get('/') .get('/')
.expect(200, 'to', done) .expect(200, 'to', done)
}) })
describe('with "acceptRanges" option', function () {
describe('when true', function () {
it('should advertise byte range accepted', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'nums.txt'), {
acceptRanges: true
})
})
request(app)
.get('/')
.expect(200)
.expect('Accept-Ranges', 'bytes')
.expect('123456789')
.end(done)
})
it('should respond to range request', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'nums.txt'), {
acceptRanges: true
})
})
request(app)
.get('/')
.set('Range', 'bytes=0-4')
.expect(206, '12345', done)
})
})
describe('when false', function () {
it('should not advertise accept-ranges', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'nums.txt'), {
acceptRanges: false
})
})
request(app)
.get('/')
.expect(200)
.expect(utils.shouldNotHaveHeader('Accept-Ranges'))
.end(done)
})
it('should not honor range requests', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'nums.txt'), {
acceptRanges: false
})
})
request(app)
.get('/')
.set('Range', 'bytes=0-4')
.expect(200, '123456789', done)
})
})
})
describe('with "cacheControl" option', function () {
describe('when true', function () {
it('should send cache-control header', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
cacheControl: true
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=0')
.end(done)
})
})
describe('when false', function () {
it('should not send cache-control header', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
cacheControl: false
})
})
request(app)
.get('/')
.expect(200)
.expect(utils.shouldNotHaveHeader('Cache-Control'))
.end(done)
})
})
})
describe('with "dotfiles" option', function () {
describe('when "allow"', function () {
it('should allow dotfiles', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, '.name'), {
dotfiles: 'allow'
})
})
request(app)
.get('/')
.expect(200)
.expect(utils.shouldHaveBody(Buffer.from('tobi')))
.end(done)
})
})
describe('when "deny"', function () {
it('should deny dotfiles', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, '.name'), {
dotfiles: 'deny'
})
})
request(app)
.get('/')
.expect(403)
.expect(/Forbidden/)
.end(done)
})
})
describe('when "ignore"', function () {
it('should ignore dotfiles', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, '.name'), {
dotfiles: 'ignore'
})
})
request(app)
.get('/')
.expect(404)
.expect(/Not Found/)
.end(done)
})
})
})
describe('with "headers" option', function () {
it('should set headers on response', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
headers: {
'X-Foo': 'Bar',
'X-Bar': 'Foo'
}
})
})
request(app)
.get('/')
.expect(200)
.expect('X-Foo', 'Bar')
.expect('X-Bar', 'Foo')
.end(done)
})
it('should use last header when duplicated', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
headers: {
'X-Foo': 'Bar',
'x-foo': 'bar'
}
})
})
request(app)
.get('/')
.expect(200)
.expect('X-Foo', 'bar')
.end(done)
})
it('should override Content-Type', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
headers: {
'Content-Type': 'text/x-custom'
}
})
})
request(app)
.get('/')
.expect(200)
.expect('Content-Type', 'text/x-custom')
.end(done)
})
it('should not set headers on 404', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'does-not-exist'), {
headers: {
'X-Foo': 'Bar'
}
})
})
request(app)
.get('/')
.expect(404)
.expect(utils.shouldNotHaveHeader('X-Foo'))
.end(done)
})
})
describe('with "immutable" option', function () {
describe('when true', function () {
it('should send cache-control header with immutable', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
immutable: true
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=0, immutable')
.end(done)
})
})
describe('when false', function () {
it('should not send cache-control header with immutable', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
immutable: false
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=0')
.end(done)
})
})
})
describe('with "lastModified" option', function () {
describe('when true', function () {
it('should send last-modified header', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
lastModified: true
})
})
request(app)
.get('/')
.expect(200)
.expect(utils.shouldHaveHeader('Last-Modified'))
.end(done)
})
it('should conditionally respond with if-modified-since', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
lastModified: true
})
})
request(app)
.get('/')
.set('If-Modified-Since', (new Date(Date.now() + 99999).toUTCString()))
.expect(304, done)
})
})
describe('when false', function () {
it('should not have last-modified header', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
lastModified: false
})
})
request(app)
.get('/')
.expect(200)
.expect(utils.shouldNotHaveHeader('Last-Modified'))
.end(done)
})
it('should not honor if-modified-since', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
lastModified: false
})
})
request(app)
.get('/')
.set('If-Modified-Since', (new Date(Date.now() + 99999).toUTCString()))
.expect(200)
.expect(utils.shouldNotHaveHeader('Last-Modified'))
.end(done)
})
})
})
describe('with "maxAge" option', function () {
it('should set cache-control max-age to milliseconds', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
maxAge: 20000
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=20')
.end(done)
})
it('should cap cache-control max-age to 1 year', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
maxAge: 99999999999
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=31536000')
.end(done)
})
it('should min cache-control max-age to 0', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
maxAge: -20000
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=0')
.end(done)
})
it('should floor cache-control max-age', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
maxAge: 21911.23
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=21')
.end(done)
})
describe('when cacheControl: false', function () {
it('should not send cache-control', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
cacheControl: false,
maxAge: 20000
})
})
request(app)
.get('/')
.expect(200)
.expect(utils.shouldNotHaveHeader('Cache-Control'))
.end(done)
})
})
describe('when string', function () {
it('should accept plain number as milliseconds', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
maxAge: '20000'
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=20')
.end(done)
})
it('should accept suffix "s" for seconds', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
maxAge: '20s'
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=20')
.end(done)
})
it('should accept suffix "m" for minutes', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
maxAge: '20m'
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=1200')
.end(done)
})
it('should accept suffix "d" for days', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'user.html'), {
maxAge: '20d'
})
})
request(app)
.get('/')
.expect(200)
.expect('Cache-Control', 'public, max-age=1728000')
.end(done)
})
})
})
describe('with "root" option', function () {
it('should allow relative path', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile('name.txt', {
root: fixtures
})
})
request(app)
.get('/')
.expect(200, 'tobi', done)
})
it('should allow up within root', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile('fake/../name.txt', {
root: fixtures
})
})
request(app)
.get('/')
.expect(200, 'tobi', done)
})
it('should reject up outside root', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile('..' + path.sep + path.relative(path.dirname(fixtures), path.join(fixtures, 'name.txt')), {
root: fixtures
})
})
request(app)
.get('/')
.expect(403, done)
})
it('should reject reading outside root', function (done) {
var app = express()
app.use(function (req, res) {
res.sendFile('../name.txt', {
root: fixtures
})
})
request(app)
.get('/')
.expect(403, done)
})
})
}) })
}) })
@ -390,12 +902,10 @@ function createApp(path, options, fn) {
return app; return app;
} }
function shouldHaveBody (buf) { function tryRequire (name) {
return function (res) { try {
var body = !Buffer.isBuffer(res.body) return require(name)
? Buffer.from(res.text) } catch (e) {
: res.body return {}
assert.ok(body, 'response has body')
assert.strictEqual(body.toString('hex'), buf.toString('hex'))
} }
} }

View File

@ -1,21 +1,202 @@
'use strict' 'use strict'
var express = require('../') var express = require('../')
, request = require('supertest'); var request = require('supertest')
var isIoJs = process.release
? process.release.name === 'io.js'
: ['v1.', 'v2.', 'v3.'].indexOf(process.version.slice(0, 3)) !== -1
describe('res', function () { describe('res', function () {
describe('.status(code)', function () { describe('.status(code)', function () {
it('should set the response .statusCode', function(done){ describe('when "code" is undefined', function () {
var app = express(); it('should raise error for invalid status code', function (done) {
var app = express()
app.use(function (req, res) { app.use(function (req, res) {
res.status(201).end('Created'); res.status(undefined).end()
}); })
request(app) request(app)
.get('/') .get('/')
.expect('Created') .expect(500, /Invalid status code/, function (err) {
.expect(201, done); if (isIoJs) {
done(err ? null : new Error('expected error'))
} else {
done(err)
}
})
})
})
describe('when "code" is null', function () {
it('should raise error for invalid status code', function (done) {
var app = express()
app.use(function (req, res) {
res.status(null).end()
})
request(app)
.get('/')
.expect(500, /Invalid status code/, function (err) {
if (isIoJs) {
done(err ? null : new Error('expected error'))
} else {
done(err)
}
})
})
})
describe('when "code" is 201', function () {
it('should set the response status code to 201', function (done) {
var app = express()
app.use(function (req, res) {
res.status(201).end()
})
request(app)
.get('/')
.expect(201, done)
})
})
describe('when "code" is 302', function () {
it('should set the response status code to 302', function (done) {
var app = express()
app.use(function (req, res) {
res.status(302).end()
})
request(app)
.get('/')
.expect(302, done)
})
})
describe('when "code" is 403', function () {
it('should set the response status code to 403', function (done) {
var app = express()
app.use(function (req, res) {
res.status(403).end()
})
request(app)
.get('/')
.expect(403, done)
})
})
describe('when "code" is 501', function () {
it('should set the response status code to 501', function (done) {
var app = express()
app.use(function (req, res) {
res.status(501).end()
})
request(app)
.get('/')
.expect(501, done)
})
})
describe('when "code" is "410"', function () {
it('should set the response status code to 410', function (done) {
var app = express()
app.use(function (req, res) {
res.status('410').end()
})
request(app)
.get('/')
.expect(410, done)
})
})
describe('when "code" is 410.1', function () {
it('should set the response status code to 410', function (done) {
var app = express()
app.use(function (req, res) {
res.status(410.1).end()
})
request(app)
.get('/')
.expect(410, function (err) {
if (isIoJs) {
done(err ? null : new Error('expected error'))
} else {
done(err)
}
})
})
})
describe('when "code" is 1000', function () {
it('should raise error for invalid status code', function (done) {
var app = express()
app.use(function (req, res) {
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)
}
})
})
})
describe('when "code" is 99', function () {
it('should raise error for invalid status code', function (done) {
var app = express()
app.use(function (req, res) {
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)
}
})
})
})
describe('when "code" is -401', function () {
it('should raise error for invalid status code', function (done) {
var app = express()
app.use(function (req, res) {
res.status(-401).end()
})
request(app)
.get('/')
.expect(500, /Invalid status code/, function (err) {
if (isIoJs) {
done(err ? null : new Error('expected error'))
} else {
done(err)
}
})
})
}) })
}) })
}) })

View File

@ -13,6 +13,7 @@ var Buffer = require('safe-buffer').Buffer
*/ */
exports.shouldHaveBody = shouldHaveBody exports.shouldHaveBody = shouldHaveBody
exports.shouldHaveHeader = shouldHaveHeader
exports.shouldNotHaveBody = shouldNotHaveBody exports.shouldNotHaveBody = shouldNotHaveBody
exports.shouldNotHaveHeader = shouldNotHaveHeader; exports.shouldNotHaveHeader = shouldNotHaveHeader;
@ -33,6 +34,19 @@ function shouldHaveBody (buf) {
} }
} }
/**
* Assert that a supertest response does have a header.
*
* @param {string} header Header name to check
* @returns {function}
*/
function shouldHaveHeader (header) {
return function (res) {
assert.ok((header.toLowerCase() in res.headers), 'should have header ' + header)
}
}
/** /**
* Assert that a supertest response does not have a body. * Assert that a supertest response does not have a body.
* *