diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f5499ec..1169beb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,13 @@ name: ci on: push: branches: - - master + - master + - '2.x' + paths-ignore: + - '*.md' pull_request: + paths-ignore: + - '*.md' jobs: test: @@ -11,124 +16,19 @@ jobs: strategy: matrix: name: - - Node.js 0.8 - - Node.js 0.10 - - Node.js 0.12 - - io.js 1.x - - io.js 2.x - - io.js 3.x - - Node.js 4.x - - Node.js 5.x - - Node.js 6.x - - Node.js 7.x - - Node.js 8.x - - Node.js 9.x - - Node.js 10.x - - Node.js 11.x - - Node.js 12.x - - Node.js 13.x - - Node.js 14.x - - Node.js 15.x - - Node.js 16.x - - Node.js 17.x - Node.js 18.x - - Node.js 19.x - Node.js 20.x - - Node.js 21.x - Node.js 22.x include: - - name: Node.js 0.8 - node-version: "0.8" - npm-i: mocha@2.5.3 supertest@1.1.0 - npm-rm: nyc - - - name: Node.js 0.10 - node-version: "0.10" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: Node.js 0.12 - node-version: "0.12" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: io.js 1.x - node-version: "1" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: io.js 2.x - node-version: "2" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: io.js 3.x - node-version: "3" - npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 - - - name: Node.js 4.x - node-version: "4" - npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 - - - name: Node.js 5.x - node-version: "5" - npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 - - - name: Node.js 6.x - node-version: "6" - npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 - - - name: Node.js 7.x - node-version: "7" - npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 - - - name: Node.js 8.x - node-version: "8" - npm-i: mocha@7.2.0 nyc@14.1.1 - - - name: Node.js 9.x - node-version: "9" - npm-i: mocha@7.2.0 nyc@14.1.1 - - - name: Node.js 10.x - node-version: "10" - npm-i: mocha@8.4.0 - - - name: Node.js 11.x - node-version: "11" - npm-i: mocha@8.4.0 - - - name: Node.js 12.x - node-version: "12" - npm-i: mocha@9.2.2 - - - name: Node.js 13.x - node-version: "13" - npm-i: mocha@9.2.2 - - - name: Node.js 14.x - node-version: "14" - - - name: Node.js 15.x - node-version: "15" - - - name: Node.js 16.x - node-version: "16" - - - name: Node.js 17.x - node-version: "17" - - name: Node.js 18.x node-version: "18" - - name: Node.js 19.x - node-version: "19" - - name: Node.js 20.x node-version: "20" - - name: Node.js 21.x - node-version: "21" - - name: Node.js 22.x - node-version: "22.4.1" + node-version: "22" steps: - uses: actions/checkout@v4 @@ -137,14 +37,6 @@ jobs: shell: bash -eo pipefail -l {0} run: | nvm install --default ${{ matrix.node-version }} - if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then - nvm install --alias=npm 0.10 - nvm use ${{ matrix.node-version }} - sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" - npm config set strict-ssl false - npm install -g --prefix "$(which node)/../.." npm@1.2.8000 - sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" - fi dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" - name: Configure npm @@ -155,26 +47,6 @@ jobs: npm config set shrinkwrap false fi - - name: Remove npm module(s) ${{ matrix.npm-rm }} - run: npm rm --silent --save-dev ${{ matrix.npm-rm }} - if: matrix.npm-rm != '' - - - name: Install npm module(s) ${{ matrix.npm-i }} - run: npm install --save-dev ${{ matrix.npm-i }} - if: matrix.npm-i != '' - - - name: Setup Node.js version-specific dependencies - shell: bash - run: | - # eslint for linting - # - remove on Node.js < 12 - if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then - node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ - grep -E '^eslint(-|$)' | \ - sort -r | \ - xargs -n1 npm rm --silent --save-dev - fi - - name: Install Node.js dependencies run: npm install @@ -190,19 +62,14 @@ jobs: - name: Run tests shell: bash run: | - if npm -ps ls nyc | grep -q nyc; then - npm run test-ci - cp coverage/lcov.info "coverage/${{ matrix.name }}.lcov" - else - npm test - fi + 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 - if: steps.list_env.outputs.nyc != '' run: | if [[ -d ./coverage ]]; then mv ./coverage "./${{ matrix.name }}" @@ -212,7 +79,6 @@ jobs: - name: Upload code coverage uses: actions/upload-artifact@v3 - if: steps.list_env.outputs.nyc != '' with: name: coverage path: ./coverage diff --git a/HISTORY.md b/HISTORY.md index 81d23e06..a1c98849 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,10 +1,41 @@ +2.1.0 / 2024-09-10 +========================= +* Propagate changes from 1.20.3 +* add brotli support #406 + +2.0.0-beta.2 / 2023-02-23 +========================= + +This incorporates all changes after 1.19.1 up to 1.20.2. + + * Remove deprecated `bodyParser()` combination middleware + * deps: debug@3.1.0 + - Add `DEBUG_HIDE_DATE` environment variable + - Change timer to per-namespace instead of global + - Change non-TTY date format + - Remove `DEBUG_FD` environment variable support + - Support 256 namespace colors + * deps: iconv-lite@0.5.2 + - Add encoding cp720 + - Add encoding UTF-32 + * deps: raw-body@3.0.0-beta.1 + +2.0.0-beta.1 / 2021-12-17 +========================= + + * Drop support for Node.js 0.8 + * `req.body` is no longer always initialized to `{}` + - it is left `undefined` unless a body is parsed + * `urlencoded` parser now defaults `extended` to `false` + * Use `on-finished` to determine when body read + 1.20.3 / 2024-09-10 =================== * deps: qs@6.13.0 * add `depth` option to customize the depth level in the parser * IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`) - + 1.20.2 / 2023-02-21 =================== diff --git a/README.md b/README.md index f6661b7d..1eebdffd 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,7 @@ var bodyParser = require('body-parser') The `bodyParser` object exposes various factories to create middlewares. All middlewares will populate the `req.body` property with the parsed body when -the `Content-Type` request header matches the `type` option, or an empty -object (`{}`) if there was no body to parse, the `Content-Type` was not matched, -or an error occurred. +the `Content-Type` request header matches the `type` option. The various errors returned by this module are described in the [errors section](#errors). @@ -67,8 +65,8 @@ The various errors returned by this module are described in the Returns middleware that only parses `json` and only looks at requests where the `Content-Type` header matches the `type` option. This parser accepts any -Unicode encoding of the body and supports automatic inflation of `gzip` and -`deflate` encodings. +Unicode encoding of the body and supports automatic inflation of `gzip`, +`br` (brotli) and `deflate` encodings. A new `body` object containing the parsed data is populated on the `request` object after the middleware (i.e. `req.body`). @@ -122,7 +120,8 @@ encoding of the request. The parsing can be aborted by throwing an error. Returns middleware that parses all bodies as a `Buffer` and only looks at requests where the `Content-Type` header matches the `type` option. This -parser supports automatic inflation of `gzip` and `deflate` encodings. +parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate` +encodings. A new `body` object containing the parsed data is populated on the `request` object after the middleware (i.e. `req.body`). This will be a `Buffer` object @@ -167,7 +166,8 @@ encoding of the request. The parsing can be aborted by throwing an error. Returns middleware that parses all bodies as a string and only looks at requests where the `Content-Type` header matches the `type` option. This -parser supports automatic inflation of `gzip` and `deflate` encodings. +parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate` +encodings. A new `body` string containing the parsed data is populated on the `request` object after the middleware (i.e. `req.body`). This will be a string of the @@ -217,7 +217,7 @@ encoding of the request. The parsing can be aborted by throwing an error. Returns middleware that only parses `urlencoded` bodies and only looks at requests where the `Content-Type` header matches the `type` option. This parser accepts only UTF-8 encoding of the body and supports automatic -inflation of `gzip` and `deflate` encodings. +inflation of `gzip`, `br` (brotli) and `deflate` encodings. A new `body` object containing the parsed data is populated on the `request` object after the middleware (i.e. `req.body`). This object will contain @@ -231,16 +231,12 @@ any of the following keys: ##### extended -The `extended` option allows to choose between parsing the URL-encoded data -with the `querystring` library (when `false`) or the `qs` library (when -`true`). The "extended" syntax allows for rich objects and arrays to be -encoded into the URL-encoded format, allowing for a JSON-like experience -with URL-encoded. For more information, please -[see the qs library](https://www.npmjs.org/package/qs#readme). +The "extended" syntax allows for rich objects and arrays to be encoded into the +URL-encoded format, allowing for a JSON-like experience with URL-encoded. For +more information, please [see the qs +library](https://www.npmjs.org/package/qs#readme). -Defaults to `true`, but using the default has been deprecated. Please -research into the difference between `qs` and `querystring` and choose the -appropriate setting. +Defaults to `false`. ##### inflate @@ -278,6 +274,23 @@ The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)` where `buf` is a `Buffer` of the raw request body and `encoding` is the encoding of the request. The parsing can be aborted by throwing an error. +##### defaultCharset + +The default charset to parse as, if not specified in content-type. Must be +either `utf-8` or `iso-8859-1`. Defaults to `utf-8`. + +##### charsetSentinel + +Whether to let the value of the `utf8` parameter take precedence as the charset +selector. It requires the form to contain a parameter named `utf8` with a value +of `✓`. Defaults to `false`. + +##### interpretNumericEntities + +Whether to decode numeric entities such as `☺` when parsing an iso-8859-1 +form. Defaults to `false`. + + #### depth The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible. @@ -397,7 +410,7 @@ var bodyParser = require('body-parser') var app = express() // parse application/x-www-form-urlencoded -app.use(bodyParser.urlencoded({ extended: false })) +app.use(bodyParser.urlencoded()) // parse application/json app.use(bodyParser.json()) @@ -405,7 +418,7 @@ app.use(bodyParser.json()) app.use(function (req, res) { res.setHeader('Content-Type', 'text/plain') res.write('you posted:\n') - res.end(JSON.stringify(req.body, null, 2)) + res.end(String(JSON.stringify(req.body, null, 2))) }) ``` @@ -425,15 +438,17 @@ var app = express() var jsonParser = bodyParser.json() // create application/x-www-form-urlencoded parser -var urlencodedParser = bodyParser.urlencoded({ extended: false }) +var urlencodedParser = bodyParser.urlencoded() // POST /login gets urlencoded bodies app.post('/login', urlencodedParser, function (req, res) { + if (!req.body || !req.body.username) res.sendStatus(400) res.send('welcome, ' + req.body.username) }) // POST /api/users gets JSON bodies app.post('/api/users', jsonParser, function (req, res) { + if (!req.body) res.sendStatus(400) // create user in req.body }) ``` diff --git a/index.js b/index.js index bb24d739..81fb9046 100644 --- a/index.js +++ b/index.js @@ -6,13 +6,6 @@ 'use strict' -/** - * Module dependencies. - * @private - */ - -var deprecate = require('depd')('body-parser') - /** * Cache of loaded parsers. * @private @@ -34,8 +27,7 @@ var parsers = Object.create(null) * @type {Parsers} */ -exports = module.exports = deprecate.function(bodyParser, - 'bodyParser: use individual json/urlencoded middlewares') +exports = module.exports = bodyParser /** * JSON parser. @@ -90,26 +82,8 @@ Object.defineProperty(exports, 'urlencoded', { * @public */ -function bodyParser (options) { - // use default type for parsers - var opts = Object.create(options || null, { - type: { - configurable: true, - enumerable: true, - value: undefined, - writable: true - } - }) - - var _urlencoded = exports.urlencoded(opts) - var _json = exports.json(opts) - - return function bodyParser (req, res, next) { - _json(req, res, function (err) { - if (err) return next(err) - _urlencoded(req, res, next) - }) - } +function bodyParser () { + throw new Error('The bodyParser() generic has been split into individual middleware to use instead.') } /** diff --git a/lib/read.js b/lib/read.js index fce6283f..a4bd446d 100644 --- a/lib/read.js +++ b/lib/read.js @@ -25,6 +25,12 @@ var zlib = require('zlib') module.exports = read +/** + * @const + * whether current node version has brotli support + */ +var hasBrotliSupport = 'createBrotliDecompress' in zlib + /** * Read a request into a buffer and parse. * @@ -42,9 +48,6 @@ function read (req, res, next, parse, debug, options) { var opts = options var stream - // flag as parsed - req._body = true - // read options var encoding = opts.encoding !== null ? opts.encoding @@ -125,7 +128,7 @@ function read (req, res, next, parse, debug, options) { str = typeof body !== 'string' && encoding !== null ? iconv.decode(body, encoding) : body - req.body = parse(str) + req.body = parse(str, encoding) } catch (err) { next(createError(400, err, { body: str, @@ -177,11 +180,20 @@ function contentstream (req, debug, inflate) { stream = req stream.length = length break - default: - throw createError(415, 'unsupported content encoding "' + encoding + '"', { - encoding: encoding, - type: 'encoding.unsupported' - }) + case 'br': + if (hasBrotliSupport) { + stream = zlib.createBrotliDecompress() + debug('brotli decompress body') + req.pipe(stream) + } + break + } + + if (stream === undefined) { + throw createError(415, 'unsupported content encoding "' + encoding + '"', { + encoding: encoding, + type: 'encoding.unsupported' + }) } return stream diff --git a/lib/types/json.js b/lib/types/json.js index 59f3f7e2..30bf8cab 100644 --- a/lib/types/json.js +++ b/lib/types/json.js @@ -16,6 +16,7 @@ var bytes = require('bytes') var contentType = require('content-type') var createError = require('http-errors') var debug = require('debug')('body-parser:json') +var isFinished = require('on-finished').isFinished var read = require('../read') var typeis = require('type-is') @@ -99,13 +100,15 @@ function json (options) { } return function jsonParser (req, res, next) { - if (req._body) { + if (isFinished(req)) { debug('body already parsed') next() return } - req.body = req.body || {} + if (!('body' in req)) { + req.body = undefined + } // skip requests without bodies if (!typeis.hasBody(req)) { diff --git a/lib/types/raw.js b/lib/types/raw.js index f5d1b674..bfe274cf 100644 --- a/lib/types/raw.js +++ b/lib/types/raw.js @@ -12,6 +12,7 @@ var bytes = require('bytes') var debug = require('debug')('body-parser:raw') +var isFinished = require('on-finished').isFinished var read = require('../read') var typeis = require('type-is') @@ -53,13 +54,15 @@ function raw (options) { } return function rawParser (req, res, next) { - if (req._body) { + if (isFinished(req)) { debug('body already parsed') next() return } - req.body = req.body || {} + if (!('body' in req)) { + req.body = undefined + } // skip requests without bodies if (!typeis.hasBody(req)) { diff --git a/lib/types/text.js b/lib/types/text.js index 083a0090..b153931b 100644 --- a/lib/types/text.js +++ b/lib/types/text.js @@ -13,6 +13,7 @@ var bytes = require('bytes') var contentType = require('content-type') var debug = require('debug')('body-parser:text') +var isFinished = require('on-finished').isFinished var read = require('../read') var typeis = require('type-is') @@ -55,13 +56,15 @@ function text (options) { } return function textParser (req, res, next) { - if (req._body) { + if (isFinished(req)) { debug('body already parsed') next() return } - req.body = req.body || {} + if (!('body' in req)) { + req.body = undefined + } // skip requests without bodies if (!typeis.hasBody(req)) { diff --git a/lib/types/urlencoded.js b/lib/types/urlencoded.js index 2bd4485f..273395cb 100644 --- a/lib/types/urlencoded.js +++ b/lib/types/urlencoded.js @@ -16,9 +16,10 @@ var bytes = require('bytes') var contentType = require('content-type') var createError = require('http-errors') var debug = require('debug')('body-parser:urlencoded') -var deprecate = require('depd')('body-parser') +var isFinished = require('on-finished').isFinished var read = require('../read') var typeis = require('type-is') +var qs = require('qs') /** * Module exports. @@ -26,12 +27,6 @@ var typeis = require('type-is') module.exports = urlencoded -/** - * Cache of parser modules. - */ - -var parsers = Object.create(null) - /** * Create a middleware to parse urlencoded bodies. * @@ -43,50 +38,53 @@ var parsers = Object.create(null) function urlencoded (options) { var opts = options || {} - // notice because option default will flip in next major - if (opts.extended === undefined) { - deprecate('undefined extended: provide extended option') - } - - var extended = opts.extended !== false + var extended = Boolean(opts.extended) var inflate = opts.inflate !== false var limit = typeof opts.limit !== 'number' ? bytes.parse(opts.limit || '100kb') : opts.limit var type = opts.type || 'application/x-www-form-urlencoded' var verify = opts.verify || false + var charsetSentinel = opts.charsetSentinel + var interpretNumericEntities = opts.interpretNumericEntities + + if (verify !== false && typeof verify !== 'function') { + throw new TypeError('option verify must be function') + } + var depth = typeof opts.depth !== 'number' ? Number(opts.depth || 32) : opts.depth - if (verify !== false && typeof verify !== 'function') { - throw new TypeError('option verify must be function') + var defaultCharset = opts.defaultCharset || 'utf-8' + if (defaultCharset !== 'utf-8' && defaultCharset !== 'iso-8859-1') { + throw new TypeError('option defaultCharset must be either utf-8 or iso-8859-1') } // create the appropriate query parser - var queryparse = extended - ? extendedparser(opts) - : simpleparser(opts) + var queryparse = createQueryParser(opts, extended) // create the appropriate type checking function var shouldParse = typeof type !== 'function' ? typeChecker(type) : type - function parse (body) { + function parse (body, encoding) { return body.length - ? queryparse(body) + ? queryparse(body, encoding) : {} } return function urlencodedParser (req, res, next) { - if (req._body) { + if (isFinished(req)) { debug('body already parsed') next() return } - req.body = req.body || {} + if (!('body' in req)) { + req.body = undefined + } // skip requests without bodies if (!typeis.hasBody(req)) { @@ -105,8 +103,8 @@ function urlencoded (options) { } // assert charset - var charset = getCharset(req) || 'utf-8' - if (charset !== 'utf-8') { + var charset = getCharset(req) || defaultCharset + if (charset !== 'utf-8' && charset !== 'iso-8859-1') { debug('invalid charset') next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', { charset: charset, @@ -122,6 +120,8 @@ function urlencoded (options) { inflate: inflate, limit: limit, verify: verify, + charsetSentinel: charsetSentinel, + interpretNumericEntities: interpretNumericEntities, depth: depth }) } @@ -133,15 +133,16 @@ function urlencoded (options) { * @param {object} options */ -function extendedparser (options) { +function createQueryParser (options, extended) { var parameterLimit = options.parameterLimit !== undefined ? options.parameterLimit : 1000 + var charsetSentinel = options.charsetSentinel + var interpretNumericEntities = options.interpretNumericEntities var depth = typeof options.depth !== 'number' ? Number(options.depth || 32) : options.depth - var parse = parser('qs') if (isNaN(parameterLimit) || parameterLimit < 1) { throw new TypeError('option parameterLimit must be a positive number') @@ -155,7 +156,7 @@ function extendedparser (options) { parameterLimit = parameterLimit | 0 } - return function queryparse (body) { + return function queryparse (body, encoding) { var paramCount = parameterCount(body, parameterLimit) if (paramCount === undefined) { @@ -165,16 +166,19 @@ function extendedparser (options) { }) } - var arrayLimit = Math.max(100, paramCount) + var arrayLimit = extended ? Math.max(100, paramCount) : 0 - debug('parse extended urlencoding') + debug('parse ' + (extended ? 'extended ' : '') + 'urlencoding') try { - return parse(body, { + return qs.parse(body, { allowPrototypes: true, arrayLimit: arrayLimit, depth: depth, - strictDepth: true, - parameterLimit: parameterLimit + charsetSentinel: charsetSentinel, + interpretNumericEntities: interpretNumericEntities, + charset: encoding, + parameterLimit: parameterLimit, + strictDepth: true }) } catch (err) { if (err instanceof RangeError) { @@ -227,72 +231,6 @@ function parameterCount (body, limit) { return count } -/** - * Get parser for module name dynamically. - * - * @param {string} name - * @return {function} - * @api private - */ - -function parser (name) { - var mod = parsers[name] - - if (mod !== undefined) { - return mod.parse - } - - // this uses a switch for static require analysis - switch (name) { - case 'qs': - mod = require('qs') - break - case 'querystring': - mod = require('querystring') - break - } - - // store to prevent invoking require() - parsers[name] = mod - - return mod.parse -} - -/** - * Get the simple query parser. - * - * @param {object} options - */ - -function simpleparser (options) { - var parameterLimit = options.parameterLimit !== undefined - ? options.parameterLimit - : 1000 - var parse = parser('querystring') - - if (isNaN(parameterLimit) || parameterLimit < 1) { - throw new TypeError('option parameterLimit must be a positive number') - } - - if (isFinite(parameterLimit)) { - parameterLimit = parameterLimit | 0 - } - - return function queryparse (body) { - var paramCount = parameterCount(body, parameterLimit) - - if (paramCount === undefined) { - debug('too many parameters') - throw createError(413, 'too many parameters', { - type: 'parameters.too.many' - }) - } - - debug('parse urlencoding') - return parse(body, undefined, undefined, { maxKeys: parameterLimit }) - } -} - /** * Get the simple type checker. * diff --git a/package.json b/package.json index 3c9926fc..f16cc5d6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "body-parser", "description": "Node.js body parsing middleware", - "version": "1.20.3", + "version": "2.1.0", "contributors": [ "Douglas Christopher Wilson ", "Jonathan Ong (http://jongleberry.com)" @@ -11,14 +11,13 @@ "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", + "debug": "3.1.0", "destroy": "1.2.0", "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "iconv-lite": "0.5.2", "on-finished": "2.4.1", "qs": "6.13.0", - "raw-body": "2.5.2", + "raw-body": "^3.0.0", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -44,8 +43,7 @@ "index.js" ], "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">= 0.10" }, "scripts": { "lint": "eslint .", diff --git a/test/body-parser.js b/test/body-parser.js index 2d764d3a..0297471c 100644 --- a/test/body-parser.js +++ b/test/body-parser.js @@ -1,167 +1,11 @@ +'use strict' -var http = require('http') -var methods = require('methods') -var request = require('supertest') +var assert = require('assert') var bodyParser = require('..') describe('bodyParser()', function () { - before(function () { - this.server = createServer() - }) - - it('should default to {}', function (done) { - request(this.server) - .post('/') - .expect(200, '{}', done) - }) - - it('should parse JSON', function (done) { - request(this.server) - .post('/') - .set('Content-Type', 'application/json') - .send('{"user":"tobi"}') - .expect(200, '{"user":"tobi"}', done) - }) - - it('should parse x-www-form-urlencoded', function (done) { - request(this.server) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send('user=tobi') - .expect(200, '{"user":"tobi"}', done) - }) - - it('should handle duplicated middleware', function (done) { - var _bodyParser = bodyParser() - var server = http.createServer(function (req, res) { - _bodyParser(req, res, function (err0) { - _bodyParser(req, res, function (err1) { - var err = err0 || err1 - res.statusCode = err ? (err.status || 500) : 200 - res.end(err ? err.message : JSON.stringify(req.body)) - }) - }) - }) - - request(server) - .post('/') - .set('Content-Type', 'application/json') - .send('{"user":"tobi"}') - .expect(200, '{"user":"tobi"}', done) - }) - - describe('http methods', function () { - before(function () { - var _bodyParser = bodyParser() - - this.server = http.createServer(function (req, res) { - _bodyParser(req, res, function (err) { - if (err) { - res.statusCode = 500 - res.end(err.message) - return - } - - res.statusCode = req.headers['x-expect-method'] === req.method - ? req.body.user === 'tobi' - ? 201 - : 400 - : 405 - res.end() - }) - }) - }) - - function getMajorVersion (versionString) { - return versionString.split('.')[0] - } - - function shouldSkipQuery (versionString) { - // Skipping HTTP QUERY tests on Node 21, it is reported in http.METHODS on 21.7.2 but not supported - // update this implementation to run on supported versions of 21 once they exist - // upstream tracking https://github.com/nodejs/node/issues/51562 - // express tracking issue: https://github.com/expressjs/express/issues/5615 - return getMajorVersion(versionString) === '21' - } - - methods.slice().sort().forEach(function (method) { - if (method === 'connect') return - - it('should support ' + method.toUpperCase() + ' requests', function (done) { - if (method === 'query' && shouldSkipQuery(process.versions.node)) { - this.skip() - } - request(this.server)[method]('/') - .set('Content-Type', 'application/json') - .set('Content-Length', '15') - .set('X-Expect-Method', method.toUpperCase()) - .send('{"user":"tobi"}') - .expect(201, done) - }) - }) - }) - - describe('with type option', function () { - before(function () { - this.server = createServer({ limit: '1mb', type: 'application/octet-stream' }) - }) - - it('should parse JSON', function (done) { - request(this.server) - .post('/') - .set('Content-Type', 'application/json') - .send('{"user":"tobi"}') - .expect(200, '{"user":"tobi"}', done) - }) - - it('should parse x-www-form-urlencoded', function (done) { - request(this.server) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send('user=tobi') - .expect(200, '{"user":"tobi"}', done) - }) - }) - - describe('with verify option', function () { - it('should apply to json', function (done) { - var server = createServer({ - verify: function (req, res, buf) { - if (buf[0] === 0x20) throw new Error('no leading space') - } - }) - - request(server) - .post('/') - .set('Content-Type', 'application/json') - .send(' {"user":"tobi"}') - .expect(403, '[entity.verify.failed] no leading space', done) - }) - - it('should apply to urlencoded', function (done) { - var server = createServer({ - verify: function (req, res, buf) { - if (buf[0] === 0x20) throw new Error('no leading space') - } - }) - - request(server) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(' user=tobi') - .expect(403, '[entity.verify.failed] no leading space', done) - }) + it('should throw an error', function () { + assert.throws(bodyParser, /bodyParser\(\) generic has been split/) }) }) - -function createServer (opts) { - var _bodyParser = bodyParser(opts) - - return http.createServer(function (req, res) { - _bodyParser(req, res, function (err) { - res.statusCode = err ? (err.status || 500) : 200 - res.end(err ? ('[' + err.type + '] ' + err.message) : JSON.stringify(req.body)) - }) - }) -} diff --git a/test/json.js b/test/json.js index c76ea138..3dcfd9c0 100644 --- a/test/json.js +++ b/test/json.js @@ -1,3 +1,4 @@ +'use strict' var assert = require('assert') var asyncHooks = tryRequire('async_hooks') @@ -11,6 +12,10 @@ var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' ? describe : describe.skip +var hasBrotliSupport = 'createBrotliDecompress' in require('zlib') +var brotlit = hasBrotliSupport ? it : it.skip +var nobrotlit = !hasBrotliSupport ? it : it.skip + describe('bodyParser.json()', function () { it('should parse JSON', function (done) { request(createServer()) @@ -41,7 +46,7 @@ describe('bodyParser.json()', function () { .get('/') .set('Content-Type', 'application/json') .unset('Transfer-Encoding') - .expect(200, '{}', done) + .expect(200, 'undefined', done) }) it('should 400 when only whitespace', function (done) { @@ -66,7 +71,7 @@ describe('bodyParser.json()', function () { .expect(400, /content length/, done) }) - it('should 500 if stream not readable', function (done) { + it('should handle consumed request', function (done) { var jsonParser = bodyParser.json() var server = createServer(function (req, res, next) { req.on('end', function () { @@ -79,7 +84,7 @@ describe('bodyParser.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('{"user":"tobi"}') - .expect(500, '[stream.not.readable] stream is not readable', done) + .expect(200, 'undefined', done) }) it('should handle duplicated middleware', function (done) { @@ -324,7 +329,7 @@ describe('bodyParser.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('{"user":"tobi"}') - .expect(200, '{}', done) + .expect(200, 'undefined', done) }) }) @@ -356,7 +361,7 @@ describe('bodyParser.json()', function () { .post('/') .set('Content-Type', 'application/x-json') .send('{"user":"tobi"}') - .expect(200, '{}', done) + .expect(200, 'undefined', done) }) }) @@ -551,7 +556,7 @@ describe('bodyParser.json()', function () { .send('buzz') .expect(200) .expect('x-store-foo', 'bar') - .expect('{}') + .expect('undefined') .end(done) }) @@ -616,6 +621,13 @@ describe('bodyParser.json()', function () { test.expect(200, '{"name":"论"}', done) }) + it('should parse utf-32', function (done) { + var test = request(this.server).post('/') + test.set('Content-Type', 'application/json; charset=utf-32') + test.write(Buffer.from('fffe00007b000000220000006e000000610000006d00000065000000220000003a00000022000000ba8b0000220000007d000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + it('should parse when content-length != char length', function (done) { var test = request(this.server).post('/') test.set('Content-Type', 'application/json; charset=utf-8') @@ -675,6 +687,22 @@ describe('bodyParser.json()', function () { test.expect(200, '{"name":"论"}', done) }) + brotlit('should support brotli encoding', function (done) { + var test = request(this.server).post('/') + test.set('Content-Encoding', 'br') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('8b06807b226e616d65223a22e8aeba227d03', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + nobrotlit('should throw 415 if there\'s no brotli support', function (done) { + var test = request(this.server).post('/') + test.set('Content-Encoding', 'br') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('8b06807b226e616d65223a22e8aeba227d03', 'hex')) + test.expect(415, 'unsupported content encoding "br"', done) + }) + it('should be case-insensitive', function (done) { var test = request(this.server).post('/') test.set('Content-Encoding', 'GZIP') @@ -726,7 +754,7 @@ function createServer (opts) { : ('[' + err.type + '] ' + err.message)) } else { res.statusCode = 200 - res.end(JSON.stringify(req.body)) + res.end(JSON.stringify(req.body) || typeof req.body) } }) }) diff --git a/test/raw.js b/test/raw.js index d2ab027f..bd0b564c 100644 --- a/test/raw.js +++ b/test/raw.js @@ -1,3 +1,4 @@ +'use strict' var assert = require('assert') var asyncHooks = tryRequire('async_hooks') @@ -11,6 +12,10 @@ var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' ? describe : describe.skip +var hasBrotliSupport = 'createBrotliDecompress' in require('zlib') +var brotlit = hasBrotliSupport ? it : it.skip +var nobrotlit = !hasBrotliSupport ? it : it.skip + describe('bodyParser.raw()', function () { before(function () { this.server = createServer() @@ -55,7 +60,7 @@ describe('bodyParser.raw()', function () { .expect(200, 'buf:', done) }) - it('should 500 if stream not readable', function (done) { + it('should handle consumed stream', function (done) { var rawParser = bodyParser.raw() var server = createServer(function (req, res, next) { req.on('end', function () { @@ -68,7 +73,7 @@ describe('bodyParser.raw()', function () { .post('/') .set('Content-Type', 'application/octet-stream') .send('the user is tobi') - .expect(500, '[stream.not.readable] stream is not readable', done) + .expect(200, 'undefined', done) }) it('should handle duplicated middleware', function (done) { @@ -207,7 +212,7 @@ describe('bodyParser.raw()', function () { var test = request(this.server).post('/') test.set('Content-Type', 'application/octet-stream') test.write(Buffer.from('000102', 'hex')) - test.expect(200, '{}', done) + test.expect(200, 'undefined', done) }) }) @@ -236,7 +241,7 @@ describe('bodyParser.raw()', function () { var test = request(this.server).post('/') test.set('Content-Type', 'application/x-foo') test.write(Buffer.from('000102', 'hex')) - test.expect(200, '{}', done) + test.expect(200, 'undefined', done) }) }) @@ -369,7 +374,7 @@ describe('bodyParser.raw()', function () { .send('buzz') .expect(200) .expect('x-store-foo', 'bar') - .expect('{}') + .expect('undefined') .end(done) }) @@ -454,6 +459,22 @@ describe('bodyParser.raw()', function () { test.expect(200, 'buf:6e616d653de8aeba', done) }) + brotlit('should support brotli encoding', function (done) { + var test = request(this.server).post('/') + test.set('Content-Encoding', 'br') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('8b03806e616d653de8aeba03', 'hex')) + test.expect(200, 'buf:6e616d653de8aeba', done) + }) + + nobrotlit('should throw 415 if there\'s no brotli support', function (done) { + var test = request(this.server).post('/') + test.set('Content-Encoding', 'br') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('8b03806e616d653de8aeba03', 'hex')) + test.expect(415, 'unsupported content encoding "br"', done) + }) + it('should be case-insensitive', function (done) { var test = request(this.server).post('/') test.set('Content-Encoding', 'GZIP') @@ -490,7 +511,7 @@ function createServer (opts) { return } - res.end(JSON.stringify(req.body)) + res.end(JSON.stringify(req.body) || typeof req.body) }) }) } diff --git a/test/text.js b/test/text.js index c3c55180..4eae37ac 100644 --- a/test/text.js +++ b/test/text.js @@ -1,3 +1,4 @@ +'use strict' var assert = require('assert') var asyncHooks = tryRequire('async_hooks') @@ -11,6 +12,10 @@ var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' ? describe : describe.skip +var hasBrotliSupport = 'createBrotliDecompress' in require('zlib') +var brotlit = hasBrotliSupport ? it : it.skip +var nobrotlit = !hasBrotliSupport ? it : it.skip + describe('bodyParser.text()', function () { before(function () { this.server = createServer() @@ -55,7 +60,7 @@ describe('bodyParser.text()', function () { .expect(200, '""', done) }) - it('should 500 if stream not readable', function (done) { + it('should handle consumed stream', function (done) { var textParser = bodyParser.text() var server = createServer(function (req, res, next) { req.on('end', function () { @@ -68,7 +73,7 @@ describe('bodyParser.text()', function () { .post('/') .set('Content-Type', 'text/plain') .send('user is tobi') - .expect(500, '[stream.not.readable] stream is not readable', done) + .expect(200, 'undefined', done) }) it('should handle duplicated middleware', function (done) { @@ -230,7 +235,7 @@ describe('bodyParser.text()', function () { .post('/') .set('Content-Type', 'text/plain') .send('user is tobi') - .expect(200, '{}', done) + .expect(200, 'undefined', done) }) }) @@ -260,7 +265,7 @@ describe('bodyParser.text()', function () { .post('/') .set('Content-Type', 'text/xml') .send('tobi') - .expect(200, '{}', done) + .expect(200, 'undefined', done) }) }) @@ -410,7 +415,7 @@ describe('bodyParser.text()', function () { .send('buzz') .expect(200) .expect('x-store-foo', 'bar') - .expect('{}') + .expect('undefined') .end(done) }) @@ -524,6 +529,22 @@ describe('bodyParser.text()', function () { test.expect(200, '"name is 论"', done) }) + brotlit('should support brotli encoding', function (done) { + var test = request(this.server).post('/') + test.set('Content-Encoding', 'br') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('0b05806e616d6520697320e8aeba03', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + nobrotlit('should throw 415 if there\'s no brotli support', function (done) { + var test = request(this.server).post('/') + test.set('Content-Encoding', 'br') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('0b05806e616d6520697320e8aeba03', 'hex')) + test.expect(415, 'unsupported content encoding "br"', done) + }) + it('should be case-insensitive', function (done) { var test = request(this.server).post('/') test.set('Content-Encoding', 'GZIP') @@ -550,7 +571,10 @@ function createServer (opts) { return http.createServer(function (req, res) { _bodyParser(req, res, function (err) { res.statusCode = err ? (err.status || 500) : 200 - res.end(err ? ('[' + err.type + '] ' + err.message) : JSON.stringify(req.body)) + res.end(err + ? ('[' + err.type + '] ' + err.message) + : (JSON.stringify(req.body) || typeof req.body) + ) }) }) } diff --git a/test/urlencoded.js b/test/urlencoded.js index 970c1e12..dbf9841f 100644 --- a/test/urlencoded.js +++ b/test/urlencoded.js @@ -1,3 +1,4 @@ +'use strict' var assert = require('assert') var asyncHooks = tryRequire('async_hooks') @@ -11,6 +12,10 @@ var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' ? describe : describe.skip +var hasBrotliSupport = 'createBrotliDecompress' in require('zlib') +var brotlit = hasBrotliSupport ? it : it.skip +var nobrotlit = !hasBrotliSupport ? it : it.skip + describe('bodyParser.urlencoded()', function () { before(function () { this.server = createServer() @@ -47,6 +52,74 @@ describe('bodyParser.urlencoded()', function () { .expect(200, '{}', done) }) + var extendedValues = [true, false] + extendedValues.forEach(function (extended) { + describe('in ' + (extended ? 'extended' : 'simple') + ' mode', function () { + it.skip('should parse x-www-form-urlencoded with an explicit iso-8859-1 encoding', function (done) { + var server = createServer({ extended: extended }) + request(server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded; charset=iso-8859-1') + .send('%A2=%BD') + .expect(200, '{"¢":"½"}', done) + }) + + it('should parse x-www-form-urlencoded with unspecified iso-8859-1 encoding when the defaultCharset is set to iso-8859-1', function (done) { + var server = createServer({ defaultCharset: 'iso-8859-1', extended: extended }) + request(server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('%A2=%BD') + .expect(200, '{"¢":"½"}', done) + }) + + it('should parse x-www-form-urlencoded with an unspecified iso-8859-1 encoding when the utf8 sentinel has a value of %26%2310003%3B', function (done) { + var server = createServer({ charsetSentinel: true, extended: extended }) + request(server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('utf8=%26%2310003%3B&user=%C3%B8') + .expect(200, '{"user":"ø"}', done) + }) + + it('should parse x-www-form-urlencoded with an unspecified utf-8 encoding when the utf8 sentinel has a value of %E2%9C%93 and the defaultCharset is iso-8859-1', function (done) { + var server = createServer({ charsetSentinel: true, extended: extended }) + request(server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('utf8=%E2%9C%93&user=%C3%B8') + .expect(200, '{"user":"ø"}', done) + }) + + it('should not leave an empty string parameter when removing the utf8 sentinel from the start of the string', function (done) { + var server = createServer({ charsetSentinel: true, extended: extended }) + request(server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('utf8=%E2%9C%93&foo=bar') + .expect(200, '{"foo":"bar"}', done) + }) + + it('should not leave an empty string parameter when removing the utf8 sentinel from the middle of the string', function (done) { + var server = createServer({ charsetSentinel: true, extended: extended }) + request(server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('foo=bar&utf8=%E2%9C%93&baz=quux') + .expect(200, '{"foo":"bar","baz":"quux"}', done) + }) + + it('should not leave an empty string parameter when removing the utf8 sentinel from the end of the string', function (done) { + var server = createServer({ charsetSentinel: true, extended: extended }) + request(server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('foo=bar&baz=quux&utf8=%E2%9C%93') + .expect(200, '{"foo":"bar","baz":"quux"}', done) + }) + }) + }) + it('should handle empty message-body', function (done) { request(createServer({ limit: '1kb' })) .post('/') @@ -56,7 +129,7 @@ describe('bodyParser.urlencoded()', function () { .expect(200, '{}', done) }) - it('should 500 if stream not readable', function (done) { + it('should handle consumed stream', function (done) { var urlencodedParser = bodyParser.urlencoded() var server = createServer(function (req, res, next) { req.on('end', function () { @@ -69,7 +142,7 @@ describe('bodyParser.urlencoded()', function () { .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=tobi') - .expect(500, '[stream.not.readable] stream is not readable', done) + .expect(200, 'undefined', done) }) it('should handle duplicated middleware', function (done) { @@ -88,7 +161,7 @@ describe('bodyParser.urlencoded()', function () { .expect(200, '{"user":"tobi"}', done) }) - it('should parse extended syntax', function (done) { + it('should not parse extended syntax', function (done) { request(this.server) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') @@ -107,7 +180,7 @@ describe('bodyParser.urlencoded()', function () { .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user[name][first]=Tobi') - .expect(200, '{"user[name][first]":"Tobi"}', done) + .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) }) it('should parse multiple key instances', function (done) { @@ -528,7 +601,7 @@ describe('bodyParser.urlencoded()', function () { .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=tobi') - .expect(200, '{}', done) + .expect(200, 'undefined', done) }) }) @@ -560,7 +633,7 @@ describe('bodyParser.urlencoded()', function () { .post('/') .set('Content-Type', 'application/x-foo') .send('user=tobi') - .expect(200, '{}', done) + .expect(200, 'undefined', done) }) }) @@ -727,7 +800,7 @@ describe('bodyParser.urlencoded()', function () { .send('buzz') .expect(200) .expect('x-store-foo', 'bar') - .expect('{}') + .expect('undefined') .end(done) }) @@ -834,6 +907,22 @@ describe('bodyParser.urlencoded()', function () { test.expect(200, '{"name":"论"}', done) }) + brotlit('should support brotli encoding', function (done) { + var test = request(this.server).post('/') + test.set('Content-Encoding', 'br') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('8b03806e616d653de8aeba03', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + nobrotlit('should throw 415 if there\'s no brotli support', function (done) { + var test = request(this.server).post('/') + test.set('Content-Encoding', 'br') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('789ccb4bcc4db57db16e17001068042f', 'hex')) + test.expect(415, 'unsupported content encoding "br"', done) + }) + it('should be case-insensitive', function (done) { var test = request(this.server).post('/') test.set('Content-Encoding', 'GZIP') @@ -881,7 +970,7 @@ function createServer (opts) { res.end('[' + err.type + '] ' + err.message) } else { res.statusCode = 200 - res.end(JSON.stringify(req.body)) + res.end(JSON.stringify(req.body) || typeof req.body) } }) })