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 13.x
- Node.js 14.x
- Node.js 15.x
- Node.js 16.x
- Node.js 17.x
- Node.js 18.x
- Node.js 19.x
- Node.js 20.x
- Node.js 21.x
include:
- name: Node.js 4.0
@ -39,7 +46,7 @@ jobs:
- name: Node.js 6.x
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
node-version: "7.10"
@ -47,11 +54,11 @@ jobs:
- name: Node.js 8.x
node-version: "8.17"
npm-i: mocha@7.2.0
npm-i: mocha@7.2.0 nyc@14.1.1
- name: Node.js 9.x
node-version: "9.11"
npm-i: mocha@7.2.0
npm-i: mocha@7.2.0 nyc@14.1.1
- name: Node.js 10.x
node-version: "10.24"
@ -63,15 +70,38 @@ jobs:
- name: Node.js 12.x
node-version: "12.22"
npm-i: mocha@9.2.2
- name: Node.js 13.x
node-version: "13.14"
npm-i: mocha@9.2.2
- name: Node.js 14.x
node-version: "14.19"
node-version: "14.20"
- name: Node.js 15.x
node-version: "15.14"
- name: Node.js 16.x
node-version: "16.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:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Node.js ${{ matrix.node-version }}
shell: bash -eo pipefail -l {0}
@ -82,7 +112,11 @@ jobs:
- name: Configure npm
run: |
npm config set loglevel error
npm config set shrinkwrap false
if [[ "$(npm config get package-lock)" == "true" ]]; then
npm config set package-lock false
else
npm config set shrinkwrap false
fi
- name: Install npm module(s) ${{ matrix.npm-i }}
run: npm install --save-dev ${{ matrix.npm-i }}
@ -95,8 +129,8 @@ jobs:
shell: bash
run: |
# eslint for linting
# - remove on Node.js < 10
if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then
# - remove on Node.js < 12
if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then
node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \
grep -E '^eslint(-|$)' | \
sort -r | \
@ -113,29 +147,52 @@ jobs:
echo "node@$(node -v)"
echo "npm@$(npm -v)"
npm -s ls ||:
(npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }'
(npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT"
- name: Run tests
shell: bash
run: npm run test-ci
run: |
npm run test-ci
cp coverage/lcov.info "coverage/${{ matrix.name }}.lcov"
- name: Lint code
if: steps.list_env.outputs.eslint != ''
run: npm run lint
- name: Collect code coverage
uses: coverallsapp/github-action@master
run: |
mv ./coverage "./${{ matrix.name }}"
mkdir ./coverage
mv "./${{ matrix.name }}" "./coverage/${{ matrix.name }}"
- name: Upload code coverage
uses: actions/upload-artifact@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
flag-name: run-${{ matrix.test_number }}
parallel: true
name: coverage
path: ./coverage
retention-days: 1
coverage:
needs: test
runs-on: ubuntu-latest
steps:
- name: Upload code coverage
- uses: actions/checkout@v4
- name: Install lcov
shell: bash
run: sudo apt-get -y install lcov
- name: Collect coverage reports
uses: actions/download-artifact@v3
with:
name: coverage
path: ./coverage
- name: Merge coverage reports
shell: bash
run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./coverage/lcov.info
- name: Upload coverage report
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -9,7 +9,7 @@ also easily visible to outsiders.
## 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
class for writing performant, spec compliant, and powerful web servers
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
- [pillarjs](http://github.com/pillarjs/): Components which make up
Express but can also be used for other web frameworks
- [jshttp](http://github.com/jshttp/): Low level http libraries
- [jshttp](http://github.com/jshttp/): Low level HTTP libraries
### 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 **Committer** is a subset of contributors who have been given write access to the repository.
* A **Project Captain** is the lead maintainer of a repository.
* A **TC (Technical Committee)** is a group of committers representing the required technical
expertise to resolve rare disputes.
* A **Triager** is a subset of contributors who have been given triage access to the repository.
@ -102,12 +103,74 @@ If a consensus cannot be reached that has no objections then a majority wins vot
is called. It is also expected that the majority of decisions made by the TC are via
a consensus seeking process and that voting is only used as a last-resort.
Resolution may involve returning the issue to committers with suggestions on how to
move forward towards a consensus. It is not expected that a meeting of the TC
Resolution may involve returning the issue to project captains with suggestions on
how to move forward towards a consensus. It is not expected that a meeting of the TC
will resolve all issues on its agenda during that meeting and may prefer to continue
the discussion happening among the committers.
the discussion happening among the project captains.
Members can be added to the TC at any time. Any committer can nominate another committer
Members can be added to the TC at any time. Any TC member can nominate another committer
to the TC and the TC uses its standard consensus seeking process to evaluate whether or
not to add this new member. Members who do not participate consistently at the level of
a majority of the other members are expected to resign.
not to add this new member. The TC will consist of a minimum of 3 active members and a
maximum of 10. If the TC should drop below 5 members the active TC members should nominate
someone new. If a TC member is stepping down, they are encouraged (but not required) to
nominate someone to take their place.
TC members will be added as admin's on the Github orgs, npm orgs, and other resources as
necessary to be effective in the role.
To remain "active" a TC member should have participation within the last 12 months and miss
no more than six consecutive TC meetings. Our goal is to increase participation, not punish
people for any lack of participation, this guideline should be only be used as such
(replace an inactive member with a new active one, for example). Members who do not meet this
are expected to step down. If A TC member does not step down, an issue can be opened in the
discussions repo to move them to inactive status. TC members who step down or are removed due
to inactivity will be moved into inactive status.
Inactive status members can become active members by self nomination if the TC is not already
larger than the maximum of 10. They will also be given preference if, while at max size, an
active member steps down.
## Project Captains
The Express TC can designate captains for individual projects/repos in the
organizations. These captains are responsible for being the primary
day-to-day maintainers of the repo on a technical and community front.
Repo captains are empowered with repo ownership and package publication rights.
When there are conflicts, especially on topics that effect the Express project
at large, captains are responsible to raise it up to the TC and drive
those conflicts to resolution. Captains are also responsible for making sure
community members follow the community guidelines, maintaining the repo
and the published package, as well as in providing user support.
Like TC members, Repo captains are a subset of committers.
To become a captain for a project the candidate is expected to participate in that
project for at least 6 months as a committer prior to the request. They should have
helped with code contributions as well as triaging issues. They are also required to
have 2FA enabled on both their GitHub and npm accounts. Any TC member or existing
captain on the repo can nominate another committer to the captain role, submit a PR to
this doc, under `Current Project Captains` section (maintaining the sort order) with
the project, their GitHub handle and npm username (if different). The PR will require
at least 2 approvals from TC members and 2 weeks hold time to allow for comment and/or
dissent. When the PR is merged, a TC member will add them to the proper GitHub/npm groups.
### Current Project Captains
- `expressjs/express`: @wesleytodd
- `expressjs/discussions`: @wesleytodd
- `expressjs/expressjs.com`: @crandmck
- `expressjs/body-parser`: @wesleytodd
- `expressjs/multer`: @LinusU
- `expressjs/cookie-parser`: @wesleytodd
- `expressjs/generator`: @wesleytodd
- `expressjs/statusboard`: @wesleytodd
- `pillarjs/path-to-regexp`: @blakeembrey
- `pillarjs/router`: @dougwilson, @wesleytodd
- `pillarjs/finalhandler`: @wesleytodd
- `pillarjs/request`: @wesleytodd
- `jshttp/http-errors`: @wesleytodd
- `jshttp/cookie`: @wesleytodd
- `jshttp/on-finished`: @wesleytodd
- `jshttp/forwarded`: @wesleytodd
- `jshttp/proxy-addr`: @wesleytodd

View File

@ -162,6 +162,86 @@ This is the first Express 5.0 alpha release, based off 4.10.1.
* add:
- `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
===================
@ -2212,7 +2292,7 @@ This is the first Express 5.0 alpha release, based off 4.10.1.
* deps: connect@2.21.0
- deprecate `connect(middleware)` -- use `app.use(middleware)` instead
- deprecate `connect.createServer()` -- use `connect()` instead
- fix `res.setHeader()` patch to work with with get -> append -> set pattern
- fix `res.setHeader()` patch to work with get -> append -> set pattern
- deps: compression@~1.0.8
- deps: errorhandler@~1.1.1
- deps: express-session@~1.5.0
@ -3423,8 +3503,8 @@ Shaw]
* Added node v0.1.97 compatibility
* Added support for deleting cookies via Request#cookie('key', null)
* Updated haml submodule
* Fixed not-found page, now using using charset utf-8
* Fixed show-exceptions page, now using using charset utf-8
* Fixed not-found page, now using charset utf-8
* Fixed show-exceptions page, now using charset utf-8
* Fixed view support due to fs.readFile Buffers
* Changed; mime.type() no longer accepts ".type" due to node extname() changes
@ -3459,7 +3539,7 @@ Shaw]
==================
* Added charset support via Request#charset (automatically assigned to 'UTF-8' when respond()'s
encoding is set to 'utf8' or 'utf-8'.
encoding is set to 'utf8' or 'utf-8').
* Added "encoding" option to Request#render(). Closes #299
* Added "dump exceptions" setting, which is enabled by default.
* Added simple ejs template engine support
@ -3498,7 +3578,7 @@ Shaw]
* Added [haml.js](http://github.com/visionmedia/haml.js) submodule; removed haml-js
* Added callback function support to Request#halt() as 3rd/4th arg
* Added preprocessing of route param wildcards using param(). Closes #251
* Added view partial support (with collections etc)
* Added view partial support (with collections etc.)
* Fixed bug preventing falsey params (such as ?page=0). Closes #286
* Fixed setting of multiple cookies. Closes #199
* Changed; view naming convention is now NAME.TYPE.ENGINE (for example page.html.haml)

View File

@ -1,12 +1,10 @@
[![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 Downloads][downloads-image]][downloads-url]
[![Linux Build][ci-image]][ci-url]
[![Windows Build][appveyor-image]][appveyor-url]
[![Test Coverage][coveralls-image]][coveralls-url]
[![NPM Version][npm-version-image]][npm-url]
[![NPM Install Size][npm-install-size-image]][npm-install-size-url]
[![NPM Downloads][npm-downloads-image]][npm-downloads-url]
```js
const express = require('express')
@ -33,7 +31,7 @@ the [`npm init` command](https://docs.npmjs.com/creating-a-package-json-file).
Installation is done using the
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
```bash
```console
$ npm install express
```
@ -53,7 +51,7 @@ for more information.
## Docs & Community
* [Website and Documentation](http://expressjs.com/) - [[website repo](https://github.com/expressjs/expressjs.com)]
* [#express](https://webchat.freenode.net/?channels=express) on freenode IRC
* [#express](https://web.libera.chat/#express) on [Libera Chat](https://libera.chat) IRC
* [GitHub Organization](https://github.com/expressjs) for Official Middleware & Modules
* Visit the [Wiki](https://github.com/expressjs/express/wiki)
* [Google Group](https://groups.google.com/group/express-js) for discussion
@ -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).
### Security Issues
If you discover a security vulnerability in Express, please see [Security Policies and Procedures](Security.md).
## 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:
Install the executable. The executable's major version will match Express's:
```bash
```console
$ npm install -g express-generator@4
```
Create the app:
```bash
```console
$ express /tmp/foo && cd /tmp/foo
```
Install dependencies:
```bash
```console
$ npm install
```
Start the server:
```bash
```console
$ npm start
```
@ -109,30 +103,42 @@ $ npm start
To view the examples, clone the Express repo and install the dependencies:
```bash
$ git clone git://github.com/expressjs/express.git --depth 1
```console
$ git clone https://github.com/expressjs/express.git --depth 1
$ cd express
$ npm install
```
Then run whichever example you want:
```bash
```console
$ 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 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
@ -146,13 +152,15 @@ The current lead maintainer is [Douglas Christopher Wilson](https://github.com/d
[MIT](LICENSE)
[ci-image]: https://img.shields.io/github/workflow/status/expressjs/express/ci/master.svg?label=linux
[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-image]: https://badgen.net/appveyor/ci/dougwilson/express/master?label=windows
[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
[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
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.
Report security bugs in third-party modules to the person or team maintaining
the module. You can also report a vulnerability through the
[Node Security Project](https://nodesecurity.io/report).
the module.
## Disclosure Policy

View File

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

View File

@ -1,13 +1,17 @@
all:
@./run 1 middleware
@./run 5 middleware
@./run 10 middleware
@./run 15 middleware
@./run 20 middleware
@./run 30 middleware
@./run 50 middleware
@./run 100 middleware
@./run 1 middleware 50
@./run 5 middleware 50
@./run 10 middleware 50
@./run 15 middleware 50
@./run 20 middleware 50
@./run 30 middleware 50
@./run 50 middleware 50
@./run 100 middleware 50
@./run 10 middleware 100
@./run 10 middleware 250
@./run 10 middleware 500
@./run 10 middleware 1000
@echo
.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')
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ app.engine('md', function(path, options, fn){
app.set('views', path.join(__dirname, 'views'));
// make it the default so we dont need .md
// make it the default, so we don't need .md
app.set('view engine', 'md');
app.get('/', function(req, res){

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.
*/
var createError = require('http-errors')
var express = require('../../');
var app = module.exports = express();
@ -17,14 +18,6 @@ var users = [
, { 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
app.param(['to', 'from'], function(req, res, next, num, name){
@ -58,7 +51,7 @@ app.get('/', function(req, res){
* GET :user.
*/
app.get('/user/:user', function(req, res, next){
app.get('/user/:user', function (req, res) {
res.send('user ' + req.user.name);
});
@ -66,7 +59,7 @@ app.get('/user/:user', function(req, res, next){
* GET users :from - :to.
*/
app.get('/users/:from-:to', function(req, res, next){
app.get('/users/:from-:to', function (req, res) {
var from = req.params.from;
var to = req.params.to;
var names = users.map(function(user){ return user.name; });

View File

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

View File

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Search example</title>
<style type="text/css">
<style>
body {
font: 14px "Helvetica Neue", Helvetica;
padding: 50px;
@ -15,7 +15,7 @@
<h2>Search</h2>
<p>Try searching for "ferret" or "cat".</p>
<input type="search" name="search" value="" />
<pre />
<pre></pre>
<script src="/client.js" charset="utf-8"></script>
</body>
</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', {
title: 'Users',
count: req.count,
@ -99,7 +99,7 @@ function users2(req, res, next) {
})
}
app.get('/middleware-locals', count2, users2, function(req, res, next){
app.get('/middleware-locals', count2, users2, function (req, res) {
// you can see now how we have much less
// to pass to res.render(). If we have
// several routes related to users this

View File

@ -72,12 +72,12 @@ var userRepos = {
// and simply expose the data
// example: http://localhost:3000/api/users/?api-key=foo
app.get('/api/users', function(req, res, next){
app.get('/api/users', function (req, res) {
res.send(users);
});
// example: http://localhost:3000/api/repos/?api-key=foo
app.get('/api/repos', function(req, res, next){
app.get('/api/repos', function (req, res) {
res.send(repos);
});

View File

@ -26,6 +26,13 @@ var merge = require('utils-merge');
var resolve = require('path').resolve;
var Router = require('router');
var setPrototypeOf = require('setprototypeof')
/**
* Module variables.
* @private
*/
var hasOwnProperty = Object.prototype.hasOwnProperty
var slice = Array.prototype.slice;
/**
@ -346,7 +353,17 @@ app.param = function param(name, fn) {
app.set = function set(setting, val) {
if (arguments.length === 1) {
// 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);

View File

@ -14,6 +14,8 @@
var Buffer = require('safe-buffer').Buffer
var contentDisposition = require('content-disposition');
var createError = require('http-errors')
var deprecate = require('depd')('express');
var encodeUrl = require('encodeurl');
var escapeHtml = require('escape-html');
var http = require('http');
@ -32,6 +34,7 @@ var send = require('send');
var extname = path.extname;
var resolve = path.resolve;
var vary = require('vary');
var urlParse = require('url').parse;
/**
* Response prototype.
@ -56,6 +59,9 @@ module.exports = res
*/
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;
return this;
};
@ -180,6 +186,13 @@ res.send = function send(body) {
chunk = '';
}
// alter headers for 205
if (this.statusCode === 205) {
this.set('Content-Length', '0')
this.removeHeader('Transfer-Encoding')
chunk = ''
}
if (req.method === 'HEAD') {
// skip body for HEAD
this.end();
@ -293,7 +306,7 @@ res.jsonp = function jsonp(obj) {
*/
res.sendStatus = function sendStatus(statusCode) {
var body = statuses[statusCode] || String(statusCode)
var body = statuses.message[statusCode] || String(statusCode)
this.statusCode = statusCode;
this.type('txt');
@ -416,6 +429,13 @@ res.download = function download (path, filename, options, callback) {
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
var headers = {
'Content-Disposition': contentDisposition(name || path)
@ -437,7 +457,9 @@ res.download = function download (path, filename, options, callback) {
opts.headers = headers
// Resolve the full path for sendFile
var fullPath = resolve(path);
var fullPath = !opts.root
? resolve(path)
: path
// send file
return this.sendFile(fullPath, opts, done)
@ -532,9 +554,8 @@ res.format = function(obj){
var req = this.req;
var next = req.next;
var fn = obj.default;
if (fn) delete obj.default;
var keys = Object.keys(obj);
var keys = Object.keys(obj)
.filter(function (v) { return v !== 'default' })
var key = keys.length > 0
? req.accepts(keys)
@ -545,13 +566,12 @@ res.format = function(obj){
if (key) {
this.set('Content-Type', normalizeType(key).value);
obj[key](req, this, next);
} else if (fn) {
fn();
} else if (obj.default) {
obj.default(req, this, next)
} else {
var err = new Error('Not Acceptable');
err.status = err.statusCode = 406;
err.types = normalizeTypes(keys).map(function(o){ return o.value });
next(err);
next(createError(406, {
types: normalizeTypes(keys).map(function (o) { return o.value })
}))
}
return this;
@ -717,9 +737,13 @@ res.cookie = function (name, value, options) {
val = 's:' + sign(val, secret);
}
if ('maxAge' in opts) {
opts.expires = new Date(Date.now() + opts.maxAge);
opts.maxAge /= 1000;
if (opts.maxAge != null) {
var maxAge = opts.maxAge - 0
if (!isNaN(maxAge)) {
opts.expires = new Date(Date.now() + maxAge)
opts.maxAge = Math.floor(maxAge / 1000)
}
}
if (opts.path == null) {
@ -756,8 +780,25 @@ res.location = function location(url) {
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
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
this.format({
text: function(){
body = statuses[status] + '. Redirecting to ' + address
body = statuses.message[status] + '. Redirecting to ' + address
},
html: function(){
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(){
@ -969,7 +1010,7 @@ function sendfile(res, file, options, callback) {
* ability to escape characters that can trigger HTML sniffing.
*
* @param {*} value
* @param {function} replaces
* @param {function} replacer
* @param {number} spaces
* @param {boolean} escape
* @returns {string}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,28 +2,24 @@
var assert = require('assert')
var express = require('../')
var should = require('should')
describe('app', function(){
describe('.locals(obj)', function(){
it('should merge locals', function(){
var app = express();
should(Object.keys(app.locals)).eql(['settings'])
app.locals.user = 'tobi';
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', function () {
it('should default object', function () {
var app = express()
assert.ok(app.locals)
assert.strictEqual(typeof app.locals, 'object')
})
})
describe('.locals.settings', function(){
it('should expose app settings', function(){
var app = express();
app.set('title', 'House of Manny');
var obj = app.locals.settings;
should(obj).have.property('env', 'test')
should(obj).have.property('title', 'House of Manny')
describe('.settings', function () {
it('should contain app settings ', function () {
var app = express()
app.set('title', 'Express')
assert.ok(app.locals.settings)
assert.strictEqual(typeof app.locals.settings, 'object')
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) {
next('route');
});
app.get('/:user', function(req, res, next) {
app.get('/:user', function (req, res) {
res.send(req.params.user);
});
@ -145,11 +145,11 @@ describe('app', function(){
next(new Error('invalid invocation'))
});
app.post('/:user', function(req, res, next) {
app.post('/:user', function (req, res) {
res.send(req.params.user);
});
app.get('/:thing', function(req, res, next) {
app.get('/:thing', function (req, res) {
res.send(req.thing);
});

View File

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

View File

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

View File

@ -11,6 +11,12 @@ describe('config', function () {
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 () {
var app = express();
assert.equal(app.set('foo', 'bar'), app);
@ -21,6 +27,17 @@ describe('config', function () {
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(){
it('should throw on bad value', function(){
var app = express();
@ -51,6 +68,11 @@ describe('config', function () {
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(){
var app = express();
app.set('foo', 'bar');
@ -125,6 +147,12 @@ describe('config', function () {
assert.equal(app.enable('tobi'), app);
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(){
@ -133,6 +161,12 @@ describe('config', function () {
assert.equal(app.disable('tobi'), app);
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(){
@ -146,6 +180,11 @@ describe('config', function () {
app.set('foo', 'bar');
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(){
@ -159,5 +198,10 @@ describe('config', function () {
app.set('foo', 'bar');
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(){
it('should expose Router', function(){
express.Router.should.be.a.Function()
assert.strictEqual(typeof express.Router, 'function')
})
it('should expose json middleware', function () {
@ -35,20 +35,23 @@ describe('exports', 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(){
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(){
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(){
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){

View File

@ -1,10 +1,15 @@
'use strict'
var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer
var express = require('..')
var request = require('supertest')
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('express.json()', function () {
it('should parse JSON', function (done) {
request(createApp())
@ -38,6 +43,15 @@ describe('express.json()', function () {
.expect(200, '{}', done)
})
// The old node error message modification in body parser is catching this
it('should 400 when only whitespace', function (done) {
request(createApp())
.post('/')
.set('Content-Type', 'application/json')
.send(' \n')
.expect(400, '[entity.parse.failed] ' + parseError(' \n'), done)
})
it('should 400 when invalid content-length', function (done) {
var app = express()
@ -86,7 +100,7 @@ describe('express.json()', function () {
.post('/')
.set('Content-Type', 'application/json')
.send('{:')
.expect(400, parseError('{:'), done)
.expect(400, '[entity.parse.failed] ' + parseError('{:'), done)
})
it('should 400 for incomplete', function (done) {
@ -94,16 +108,7 @@ describe('express.json()', function () {
.post('/')
.set('Content-Type', 'application/json')
.send('{"user"')
.expect(400, 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)
.expect(400, '[entity.parse.failed] ' + parseError('{"user"'), 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-Length', '1034')
.send(JSON.stringify({ str: buf.toString() }))
.expect(413, 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)
.expect(413, '[entity.too.large] request entity too large', done)
})
it('should 413 when over limit with chunked encoding', function (done) {
var app = createApp({ limit: '1kb' })
var buf = Buffer.alloc(1024, '.')
var server = createApp({ limit: '1kb' })
var test = request(server).post('/')
var test = request(app).post('/')
test.set('Content-Type', 'application/json')
test.set('Transfer-Encoding', 'chunked')
test.write('{"str":')
@ -149,6 +143,15 @@ describe('express.json()', function () {
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) {
var buf = Buffer.alloc(1024, '.')
request(createApp({ limit: 1024 }))
@ -161,11 +164,11 @@ describe('express.json()', function () {
it('should not change when options altered', function (done) {
var buf = Buffer.alloc(1024, '.')
var options = { limit: '1kb' }
var server = createApp(options)
var app = createApp(options)
options.limit = '100kb'
request(server)
request(app)
.post('/')
.set('Content-Type', 'application/json')
.send(JSON.stringify({ str: buf.toString() }))
@ -174,14 +177,23 @@ describe('express.json()', function () {
it('should not hang response', function (done) {
var buf = Buffer.alloc(10240, '.')
var server = createApp({ limit: '8kb' })
var test = request(server).post('/')
var app = createApp({ limit: '8kb' })
var test = request(app).post('/')
test.set('Content-Type', 'application/json')
test.write(buf)
test.write(buf)
test.write(buf)
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 () {
@ -195,7 +207,7 @@ describe('express.json()', function () {
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/json')
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('/')
.set('Content-Type', 'application/json')
.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('/')
.set('Content-Type', 'application/json')
.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) {
@ -261,7 +273,7 @@ describe('express.json()', function () {
.post('/')
.set('Content-Type', 'application/json')
.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) {
@ -272,15 +284,6 @@ describe('express.json()', function () {
.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) {
request(this.app)
.post('/')
@ -288,7 +291,7 @@ describe('express.json()', function () {
.set('X-Error-Property', 'stack')
.send('true')
.expect(400)
.expect(shouldContainInBody(parseError('#rue').replace('#', 't')))
.expect(shouldContainInBody(parseError('#rue').replace(/#/g, 't')))
.end(done)
})
})
@ -397,65 +400,59 @@ describe('express.json()', function () {
})
it('should error from verify', function (done) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
} })
var app = createApp({
verify: function (req, res, buf) {
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)
.post('/')
.set('Content-Type', 'application/json')
.set('X-Error-Property', 'type')
.send('["tobi"]')
.expect(403, 'entity.verify.failed', done)
.expect(403, '[entity.verify.failed] no arrays', done)
})
it('should allow custom codes', function (done) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] !== 0x5b) return
var err = new Error('no arrays')
err.status = 400
throw err
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x5b) return
var err = new Error('no arrays')
err.status = 400
throw err
}
})
request(app)
.post('/')
.set('Content-Type', 'application/json')
.send('["tobi"]')
.expect(400, 'no arrays', done)
.expect(400, '[entity.verify.failed] no arrays', done)
})
it('should allow custom type', function (done) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] !== 0x5b) return
var err = new Error('no arrays')
err.type = 'foo.bar'
throw err
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x5b) return
var err = new Error('no arrays')
err.type = 'foo.bar'
throw err
}
})
request(app)
.post('/')
.set('Content-Type', 'application/json')
.set('X-Error-Property', 'type')
.send('["tobi"]')
.expect(403, 'foo.bar', done)
.expect(403, '[foo.bar] no arrays', done)
})
it('should include original body on error object', function (done) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
}
})
request(app)
.post('/')
@ -466,9 +463,11 @@ describe('express.json()', function () {
})
it('should allow pass-through', function (done) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
}
})
request(app)
.post('/')
@ -478,9 +477,11 @@ describe('express.json()', function () {
})
it('should work with different charsets', function (done) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
}
})
var test = request(app).post('/')
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) {
var app = createApp({ verify: function (req, res, buf) {
throw new Error('unexpected verify call')
} })
var app = createApp({
verify: function (req, res, buf) {
throw new Error('unexpected verify call')
}
})
var test = request(app).post('/')
test.set('Content-Type', 'application/json; charset=x-bogus')
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('/')
test.set('Content-Type', 'application/json; charset=koi8-r')
test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex'))
test.expect(415, '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)
test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done)
})
})
@ -599,16 +698,7 @@ describe('express.json()', function () {
test.set('Content-Encoding', 'nulls')
test.set('Content-Type', 'application/json')
test.write(Buffer.from('000000000000', 'hex'))
test.expect(415, '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)
test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done)
})
it('should 400 on malformed encoding', function (done) {
@ -638,8 +728,11 @@ function createApp (options) {
app.use(express.json(options))
app.use(function (err, req, res, next) {
// console.log(err)
res.status(err.status || 500)
res.send(String(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) {
@ -663,3 +756,11 @@ function shouldContainInBody (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'
var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer
var express = require('..')
var request = require('supertest')
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('express.raw()', function () {
before(function () {
this.app = createApp()
@ -102,6 +107,15 @@ describe('express.raw()', function () {
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) {
var buf = Buffer.alloc(1028, '.')
var app = createApp({ limit: 1024 })
@ -134,6 +148,15 @@ describe('express.raw()', function () {
test.write(buf)
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 () {
@ -147,7 +170,7 @@ describe('express.raw()', function () {
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/octet-stream')
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) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x00) throw new Error('no leading null')
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x00) throw new Error('no leading null')
}
})
var test = request(app).post('/')
test.set('Content-Type', 'application/octet-stream')
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) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] !== 0x00) return
var err = new Error('no leading null')
err.status = 400
throw err
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x00) return
var err = new Error('no leading null')
err.status = 400
throw err
}
})
var test = request(app).post('/')
test.set('Content-Type', 'application/octet-stream')
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) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x00) throw new Error('no leading null')
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x00) throw new Error('no leading null')
}
})
var test = request(app).post('/')
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 () {
before(function () {
this.app = createApp()
@ -356,12 +482,12 @@ describe('express.raw()', function () {
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('/')
test.set('Content-Encoding', 'nulls')
test.set('Content-Type', 'application/octet-stream')
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) {
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) {
@ -386,3 +514,11 @@ function createApp (options) {
return app
}
function tryRequire (name) {
try {
return require(name)
} catch (e) {
return {}
}
}

View File

@ -1,10 +1,15 @@
'use strict'
var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer
var express = require('..')
var request = require('supertest')
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('express.text()', function () {
before(function () {
this.app = createApp()
@ -75,16 +80,16 @@ describe('express.text()', function () {
describe('with defaultCharset option', function () {
it('should change default charset', function (done) {
var app = createApp({ defaultCharset: 'koi8-r' })
var test = request(app).post('/')
var server = createApp({ defaultCharset: 'koi8-r' })
var test = request(server).post('/')
test.set('Content-Type', 'text/plain')
test.write(Buffer.from('6e616d6520697320cec5d4', 'hex'))
test.expect(200, '"name is нет"', done)
})
it('should honor content-type charset', function (done) {
var app = createApp({ defaultCharset: 'koi8-r' })
var test = request(app).post('/')
var server = createApp({ defaultCharset: 'koi8-r' })
var test = request(server).post('/')
test.set('Content-Type', 'text/plain; charset=utf-8')
test.write(Buffer.from('6e616d6520697320e8aeba', 'hex'))
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) {
var buf = Buffer.alloc(1028, '.')
var app = createApp({ limit: '1kb' })
var buf = Buffer.alloc(1028, '.')
var test = request(app).post('/')
test.set('Content-Type', 'text/plain')
test.set('Transfer-Encoding', 'chunked')
@ -112,6 +117,15 @@ describe('express.text()', function () {
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) {
var buf = Buffer.alloc(1028, '.')
request(createApp({ limit: 1024 }))
@ -136,8 +150,8 @@ describe('express.text()', function () {
})
it('should not hang response', function (done) {
var buf = Buffer.alloc(10240, '.')
var app = createApp({ limit: '8kb' })
var buf = Buffer.alloc(10240, '.')
var test = request(app).post('/')
test.set('Content-Type', 'text/plain')
test.write(buf)
@ -145,6 +159,17 @@ describe('express.text()', function () {
test.write(buf)
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 () {
@ -158,7 +183,7 @@ describe('express.text()', function () {
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'text/plain')
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) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x20) throw new Error('no leading space')
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x20) throw new Error('no leading space')
}
})
request(app)
.post('/')
.set('Content-Type', 'text/plain')
.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) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] !== 0x20) return
var err = new Error('no leading space')
err.status = 400
throw err
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x20) return
var err = new Error('no leading space')
err.status = 400
throw err
}
})
request(app)
.post('/')
.set('Content-Type', 'text/plain')
.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) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x20) throw new Error('no leading space')
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x20) throw new Error('no leading space')
}
})
request(app)
.post('/')
@ -317,14 +348,109 @@ describe('express.text()', function () {
})
it('should 415 on unknown charset prior to verify', function (done) {
var app = createApp({ verify: function (req, res, buf) {
throw new Error('unexpected verify call')
} })
var app = createApp({
verify: function (req, res, buf) {
throw new Error('unexpected verify call')
}
})
var test = request(app).post('/')
test.set('Content-Type', 'text/plain; charset=x-bogus')
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('/')
test.set('Content-Type', 'text/plain; charset=x-bogus')
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)
})
it('should fail on unknown encoding', function (done) {
it('should 415 on unknown encoding', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'nulls')
test.set('Content-Type', 'text/plain')
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) {
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) {
@ -440,3 +568,11 @@ function createApp (options) {
return app
}
function tryRequire (name) {
try {
return require(name)
} catch (e) {
return {}
}
}

View File

@ -1,10 +1,15 @@
'use strict'
var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer
var express = require('..')
var request = require('supertest')
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('express.urlencoded()', function () {
before(function () {
this.app = createApp()
@ -217,7 +222,7 @@ describe('express.urlencoded()', function () {
test.set('Content-Encoding', 'gzip')
test.set('Content-Type', 'application/x-www-form-urlencoded')
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) {
var buf = Buffer.alloc(1024, '.')
var app = createApp({ limit: '1kb' })
var buf = Buffer.alloc(1024, '.')
var test = request(app).post('/')
test.set('Content-Type', 'application/x-www-form-urlencoded')
test.set('Transfer-Encoding', 'chunked')
@ -258,6 +263,15 @@ describe('express.urlencoded()', function () {
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) {
var buf = Buffer.alloc(1024, '.')
request(createApp({ limit: 1024 }))
@ -282,8 +296,8 @@ describe('express.urlencoded()', function () {
})
it('should not hang response', function (done) {
var buf = Buffer.alloc(10240, '.')
var app = createApp({ limit: '8kb' })
var buf = Buffer.alloc(10240, '.')
var test = request(app).post('/')
test.set('Content-Type', 'application/x-www-form-urlencoded')
test.write(buf)
@ -291,6 +305,15 @@ describe('express.urlencoded()', function () {
test.write(buf)
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 () {
@ -310,16 +333,7 @@ describe('express.urlencoded()', function () {
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(11))
.expect(413, /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)
.expect(413, '[parameters.too.many] too many parameters', done)
})
it('should work when at the limit', function (done) {
@ -374,16 +388,7 @@ describe('express.urlencoded()', function () {
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(11))
.expect(413, /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)
.expect(413, '[parameters.too.many] too many parameters', 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) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x20) throw new Error('no leading space')
} })
var app = createApp({
verify: function (req, res, buf) {
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)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.set('X-Error-Property', 'type')
.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) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] !== 0x20) return
var err = new Error('no leading space')
err.status = 400
throw err
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x20) return
var err = new Error('no leading space')
err.status = 400
throw err
}
})
request(app)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.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) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] !== 0x20) return
var err = new Error('no leading space')
err.type = 'foo.bar'
throw err
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] !== 0x20) return
var err = new Error('no leading space')
err.type = 'foo.bar'
throw err
}
})
request(app)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.set('X-Error-Property', 'type')
.send(' user=tobi')
.expect(403, 'foo.bar', done)
.expect(403, '[foo.bar] no leading space', done)
})
it('should allow pass-through', function (done) {
var app = createApp({ verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
} })
var app = createApp({
verify: function (req, res, buf) {
if (buf[0] === 0x5b) throw new Error('no arrays')
}
})
request(app)
.post('/')
@ -594,14 +593,109 @@ describe('express.urlencoded()', function () {
})
it('should 415 on unknown charset prior to verify', function (done) {
var app = createApp({ verify: function (req, res, buf) {
throw new Error('unexpected verify call')
} })
var app = createApp({
verify: function (req, res, buf) {
throw new Error('unexpected verify call')
}
})
var test = request(app).post('/')
test.set('Content-Type', 'application/x-www-form-urlencoded; charset=x-bogus')
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('/')
test.set('Content-Type', 'application/x-www-form-urlencoded; charset=koi8-r')
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)
})
it('should fail on unknown encoding', function (done) {
it('should 415 on unknown encoding', function (done) {
var test = request(this.app).post('/')
test.set('Content-Encoding', 'nulls')
test.set('Content-Type', 'application/x-www-form-urlencoded')
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) {
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) {
@ -733,3 +829,11 @@ function expectKeyCount (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'
var assert = require('assert')
var express = require('..')
var request = require('supertest')
var should = require('should')
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 () {
it('should append multiple headers', function (done) {
var app = express()
app.use(function (req, res, next) {
res.append('Link', '<http://localhost/>')
res.append('Set-Cookie', 'foo=bar')
next()
})
app.use(function (req, res) {
res.append('Link', '<http://localhost:80/>')
res.append('Set-Cookie', 'fizz=buzz')
res.end()
})
request(app)
.get('/')
.expect('Link', '<http://localhost/>, <http://localhost:80/>', done)
.get('/')
.expect(200)
.expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz']))
.end(done)
})
it('should accept array of values', function (done) {
@ -37,51 +35,54 @@ describe('res', function () {
})
request(app)
.get('/')
.expect(function (res) {
should(res.headers['set-cookie']).eql(['foo=bar', 'fizz=buzz'])
})
.expect(200, done)
.get('/')
.expect(200)
.expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar', 'fizz=buzz']))
.end(done)
})
it('should get reset by res.set(field, val)', function (done) {
var app = express()
app.use(function (req, res, next) {
res.append('Link', '<http://localhost/>')
res.append('Link', '<http://localhost:80/>')
res.append('Set-Cookie', 'foo=bar')
res.append('Set-Cookie', 'fizz=buzz')
next()
})
app.use(function (req, res) {
res.set('Link', '<http://127.0.0.1/>')
res.set('Set-Cookie', 'pet=tobi')
res.end()
});
request(app)
.get('/')
.expect('Link', '<http://127.0.0.1/>', done)
.get('/')
.expect(200)
.expect(shouldHaveHeaderValues('Set-Cookie', ['pet=tobi']))
.end(done)
})
it('should work with res.set(field, val) first', function (done) {
var app = express()
app.use(function (req, res, next) {
res.set('Link', '<http://localhost/>')
res.set('Set-Cookie', 'foo=bar')
next()
})
app.use(function(req, res){
res.append('Link', '<http://localhost:80/>')
res.append('Set-Cookie', 'fizz=buzz')
res.end()
})
request(app)
.get('/')
.expect('Link', '<http://localhost/>, <http://localhost:80/>', done)
.get('/')
.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()
app.use(function (req, res, next) {
@ -90,16 +91,26 @@ describe('res', function () {
})
app.use(function (req, res) {
res.append('Set-Cookie', 'bar=baz')
res.append('Set-Cookie', 'fizz=buzz')
res.end()
})
request(app)
.get('/')
.expect(function (res) {
should(res.headers['set-cookie']).eql(['foo=bar; Path=/', 'bar=baz'])
})
.expect(200, done)
.get('/')
.expect(200)
.expect(shouldHaveHeaderValues('Set-Cookie', ['foo=bar; Path=/', 'fizz=buzz']))
.end(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)
})
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(){
it('should set relative expires', function(done){
var app = express();
@ -111,6 +142,36 @@ describe('res', function(){
.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) {
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(){
it('should generate a signed JSON cookie', function(done){
var app = express();

View File

@ -1,10 +1,19 @@
'use strict'
var after = require('after');
var assert = require('assert');
var assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer
var express = require('..');
var path = require('path')
var request = require('supertest');
var utils = require('./support/utils')
var FIXTURES_PATH = path.join(__dirname, 'fixtures')
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('res', function(){
describe('.download(path)', function(){
@ -81,6 +90,272 @@ describe('res', function(){
.expect('Content-Disposition', 'attachment; filename="user.html"')
.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(){
@ -107,7 +382,7 @@ describe('res', function(){
var options = {}
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)
@ -129,12 +404,12 @@ describe('res', function(){
})
request(app)
.get('/')
.expect(200)
.expect('Content-Disposition', 'attachment; filename="document"')
.expect('Cache-Control', 'public, max-age=14400')
.expect(shouldHaveBody(Buffer.from('tobi')))
.end(done)
.get('/')
.expect(200)
.expect('Content-Disposition', 'attachment; filename="document"')
.expect('Cache-Control', 'public, max-age=14400')
.expect(utils.shouldHaveBody(Buffer.from('tobi')))
.end(done)
})
describe('when options.headers contains Content-Disposition', function () {
@ -207,25 +482,17 @@ describe('res', function(){
});
request(app)
.get('/')
.expect(shouldNotHaveHeader('Content-Disposition'))
.expect(200, 'failed', done);
.get('/')
.expect(utils.shouldNotHaveHeader('Content-Disposition'))
.expect(200, 'failed', done)
})
})
})
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 tryRequire (name) {
try {
return require(name)
} catch (e) {
return {}
}
}
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){
res.format({
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();
app4.get('/', function(req, res, next){
app4.get('/', function (req, res) {
res.format({
text: function(){ res.send('hey') },
html: function(){ res.send('<p>hey</p>') },
@ -122,6 +127,28 @@ describe('res', function(){
.set('Accept', '*/*')
.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(){
@ -132,7 +159,7 @@ describe('res', function(){
var app = express();
var router = express.Router();
router.get('/', function(req, res, next){
router.get('/', function (req, res) {
res.format({
text: function(){ res.send('hey') },
html: function(){ res.send('<p>hey</p>') },

View File

@ -1,13 +1,27 @@
'use strict'
var express = require('../')
, request = require('supertest');
, request = require('supertest')
, url = require('url');
describe('res', function(){
describe('.location(url)', function(){
it('should set the header', function(done){
var app = express();
app.use(function(req, res){
res.location('http://google.com/').end();
});
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){
res.location('http://google.com').end();
});
@ -31,6 +45,36 @@ describe('res', function(){
.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) {
var app = express()

View File

@ -1,6 +1,5 @@
'use strict'
var assert = require('assert')
var express = require('..');
var request = require('supertest');
var utils = require('./support/utils');
@ -71,11 +70,11 @@ describe('res', function(){
});
request(app)
.head('/')
.expect(302)
.expect('Location', 'http://google.com')
.expect(shouldNotHaveBody())
.end(done)
.head('/')
.expect(302)
.expect('Location', 'http://google.com')
.expect(utils.shouldNotHaveBody())
.end(done)
})
})
@ -184,20 +183,14 @@ describe('res', function(){
});
request(app)
.get('/')
.set('Accept', 'application/octet-stream')
.expect(302)
.expect('location', 'http://google.com')
.expect('content-length', '0')
.expect(utils.shouldNotHaveHeader('Content-Type'))
.expect(shouldNotHaveBody())
.end(done)
.get('/')
.set('Accept', 'application/octet-stream')
.expect(302)
.expect('location', 'http://google.com')
.expect('content-length', '0')
.expect(utils.shouldNotHaveHeader('Content-Type'))
.expect(utils.shouldNotHaveBody())
.end(done)
})
})
})
function shouldNotHaveBody () {
return function (res) {
assert.ok(res.text === '' || res.text === undefined)
}
}

View File

@ -143,11 +143,11 @@ describe('res', function(){
});
request(app)
.get('/')
.expect(200)
.expect('Content-Type', 'application/octet-stream')
.expect(shouldHaveBody(Buffer.from('hello')))
.end(done)
.get('/')
.expect(200)
.expect('Content-Type', 'application/octet-stream')
.expect(utils.shouldHaveBody(Buffer.from('hello')))
.end(done)
})
it('should set ETag', function (done) {
@ -214,10 +214,10 @@ describe('res', function(){
});
request(app)
.head('/')
.expect(200)
.expect(shouldNotHaveBody())
.end(done)
.head('/')
.expect(200)
.expect(utils.shouldNotHaveBody())
.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(){
it('should strip Content-* fields, Transfer-Encoding field, and body', function(done){
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 assert = require('assert')
var asyncHooks = tryRequire('async_hooks')
var Buffer = require('safe-buffer').Buffer
var express = require('../')
, request = require('supertest')
var onFinished = require('on-finished');
var path = require('path');
var should = require('should');
var fixtures = path.join(__dirname, 'fixtures');
var utils = require('./support/utils');
var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
? describe
: describe.skip
describe('res', function(){
describe('.sendFile(path)', function () {
it('should error missing path', function (done) {
@ -29,6 +33,14 @@ describe('res', function(){
.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) {
var app = createApp(path.resolve(fixtures, 'name.txt'));
@ -91,6 +103,23 @@ describe('res', function(){
.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) {
var app = express();
@ -132,136 +161,6 @@ describe('res', function(){
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 () {
@ -359,15 +258,71 @@ describe('res', function(){
app.use(function (req, res) {
res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) {
should(err).be.ok()
err.status.should.equal(404);
res.send('got it');
res.send(err ? 'got ' + err.status + ' error' : 'no error')
});
});
request(app)
.get('/')
.expect(200, 'got it', done);
.get('/')
.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('/')
.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;
}
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 tryRequire (name) {
try {
return require(name)
} catch (e) {
return {}
}
}

View File

@ -1,21 +1,202 @@
'use strict'
var express = require('../')
, request = require('supertest');
var request = require('supertest')
describe('res', function(){
describe('.status(code)', function(){
it('should set the response .statusCode', function(done){
var app = express();
var isIoJs = process.release
? process.release.name === 'io.js'
: ['v1.', 'v2.', 'v3.'].indexOf(process.version.slice(0, 3)) !== -1
app.use(function(req, res){
res.status(201).end('Created');
});
describe('res', function () {
describe('.status(code)', function () {
describe('when "code" is undefined', function () {
it('should raise error for invalid status code', function (done) {
var app = express()
request(app)
.get('/')
.expect('Created')
.expect(201, done);
app.use(function (req, res) {
res.status(undefined).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 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.shouldHaveHeader = shouldHaveHeader
exports.shouldNotHaveBody = shouldNotHaveBody
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.
*