diff --git a/README.md b/README.md index 1eebdffd..7efe8bc0 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,12 @@ 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. +##### experimentalZstd + +The `experimentalZstd` option enables support for automatic inflation of `zstd` +(Zstandard) encoded request bodies. Requires Node.js 23.8.0 or later. Defaults +to `false`. + ### bodyParser.raw([options]) Returns middleware that parses all bodies as a `Buffer` and only looks at @@ -162,6 +168,12 @@ 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. +##### experimentalZstd + +The `experimentalZstd` option enables support for automatic inflation of `zstd` +(Zstandard) encoded request bodies. Requires Node.js 23.8.0 or later. Defaults +to `false`. + ### bodyParser.text([options]) Returns middleware that parses all bodies as a string and only looks at @@ -212,6 +224,12 @@ 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. +##### experimentalZstd + +The `experimentalZstd` option enables support for automatic inflation of `zstd` +(Zstandard) encoded request bodies. Requires Node.js 23.8.0 or later. Defaults +to `false`. + ### bodyParser.urlencoded([options]) Returns middleware that only parses `urlencoded` bodies and only looks at @@ -290,11 +308,16 @@ of `✓`. Defaults to `false`. Whether to decode numeric entities such as `☺` when parsing an iso-8859-1 form. Defaults to `false`. - -#### depth +##### 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. +##### experimentalZstd + +The `experimentalZstd` option enables support for automatic inflation of `zstd` +(Zstandard) encoded request bodies. Requires Node.js 23.8.0 or later. Defaults +to `false`. + ## Errors The middlewares provided by this module create errors using the diff --git a/lib/read.js b/lib/read.js index eee8b111..b51d9dd7 100644 --- a/lib/read.js +++ b/lib/read.js @@ -23,6 +23,12 @@ var zlib = require('node:zlib') module.exports = read +/** + * @const + * whether current node version has zstandard support + */ +const hasZstandardSupport = 'createZstdDecompress' in zlib + /** * Read a request into a buffer and parse. * @@ -48,7 +54,7 @@ function read (req, res, next, parse, debug, options) { try { // get the content stream - stream = contentstream(req, debug, opts.inflate) + stream = contentstream(req, debug, opts.inflate, opts.experimentalZstd) length = stream.length stream.length = undefined } catch (err) { @@ -143,7 +149,7 @@ function read (req, res, next, parse, debug, options) { * @api private */ -function contentstream (req, debug, inflate) { +function contentstream (req, debug, inflate, experimentalZstd) { var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase() var length = req.headers['content-length'] @@ -161,7 +167,7 @@ function contentstream (req, debug, inflate) { return req } - var stream = createDecompressionStream(encoding, debug) + var stream = createDecompressionStream(encoding, experimentalZstd, debug) req.pipe(stream) return stream } @@ -169,11 +175,12 @@ function contentstream (req, debug, inflate) { /** * Create a decompression stream for the given encoding. * @param {string} encoding + * @param {boolean} experimentalZstd * @param {function} debug * @return {object} * @api private */ -function createDecompressionStream (encoding, debug) { +function createDecompressionStream (encoding, experimentalZstd, debug) { switch (encoding) { case 'deflate': debug('inflate body') @@ -184,12 +191,16 @@ function createDecompressionStream (encoding, debug) { case 'br': debug('brotli decompress body') return zlib.createBrotliDecompress() - default: - throw createError(415, 'unsupported content encoding "' + encoding + '"', { - encoding: encoding, - type: 'encoding.unsupported' - }) + case 'zstd': + if (hasZstandardSupport && experimentalZstd === true) { + debug('zstd decompress body') + return zlib.createZstdDecompress() + } } + throw createError(415, 'unsupported content encoding "' + encoding + '"', { + encoding: encoding, + type: 'encoding.unsupported' + }) } /** diff --git a/lib/types/json.js b/lib/types/json.js index 30bf8cab..8e5b3794 100644 --- a/lib/types/json.js +++ b/lib/types/json.js @@ -62,6 +62,7 @@ function json (options) { var strict = opts.strict !== false var type = opts.type || 'application/json' var verify = opts.verify || false + const experimentalZstd = opts.experimentalZstd === true || false if (verify !== false && typeof verify !== 'function') { throw new TypeError('option verify must be function') @@ -142,7 +143,8 @@ function json (options) { encoding: charset, inflate: inflate, limit: limit, - verify: verify + verify: verify, + experimentalZstd: experimentalZstd }) } } diff --git a/lib/types/raw.js b/lib/types/raw.js index bfe274cf..0c99d8d0 100644 --- a/lib/types/raw.js +++ b/lib/types/raw.js @@ -39,6 +39,7 @@ function raw (options) { : opts.limit var type = opts.type || 'application/octet-stream' var verify = opts.verify || false + const experimentalZstd = opts.experimentalZstd === true || false if (verify !== false && typeof verify !== 'function') { throw new TypeError('option verify must be function') @@ -85,7 +86,8 @@ function raw (options) { encoding: null, inflate: inflate, limit: limit, - verify: verify + verify: verify, + experimentalZstd: experimentalZstd }) } } diff --git a/lib/types/text.js b/lib/types/text.js index b153931b..04385b21 100644 --- a/lib/types/text.js +++ b/lib/types/text.js @@ -41,6 +41,7 @@ function text (options) { : opts.limit var type = opts.type || 'text/plain' var verify = opts.verify || false + const experimentalZstd = opts.experimentalZstd === true || false if (verify !== false && typeof verify !== 'function') { throw new TypeError('option verify must be function') @@ -90,7 +91,8 @@ function text (options) { encoding: charset, inflate: inflate, limit: limit, - verify: verify + verify: verify, + experimentalZstd: experimentalZstd }) } } diff --git a/lib/types/urlencoded.js b/lib/types/urlencoded.js index 687745f8..ca707890 100644 --- a/lib/types/urlencoded.js +++ b/lib/types/urlencoded.js @@ -47,6 +47,7 @@ function urlencoded (options) { var verify = opts.verify || false var charsetSentinel = opts.charsetSentinel var interpretNumericEntities = opts.interpretNumericEntities + const experimentalZstd = opts.experimentalZstd === true || false if (verify !== false && typeof verify !== 'function') { throw new TypeError('option verify must be function') @@ -117,7 +118,8 @@ function urlencoded (options) { limit: limit, verify: verify, charsetSentinel: charsetSentinel, - interpretNumericEntities: interpretNumericEntities + interpretNumericEntities: interpretNumericEntities, + experimentalZstd: experimentalZstd }) } } diff --git a/test/json.js b/test/json.js index e679ac57..2b8275ea 100644 --- a/test/json.js +++ b/test/json.js @@ -3,10 +3,15 @@ var assert = require('node:assert') var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage var http = require('node:http') +const zlib = require('node:zlib') var request = require('supertest') var bodyParser = require('..') +const hasZstandardSupport = 'createZstdDecompress' in zlib +const zstandardit = hasZstandardSupport ? it : it.skip +const nozstandardit = !hasZstandardSupport ? it : it.skip + describe('bodyParser.json()', function () { it('should parse JSON', function (done) { request(createServer()) @@ -686,6 +691,24 @@ describe('bodyParser.json()', function () { test.expect(200, '{"name":"论"}', done) }) + zstandardit('should support zstandard encoding', function (done) { + const server = createServer({ experimentalZstd: true, limit: '1kb' }) + var test = request(server).post('/') + test.set('Content-Encoding', 'zstd') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('28b52ffd200e7100007b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + nozstandardit('should throw 415 if there\'s no zstandard support', function (done) { + const server = createServer({ experimentalZstd: true, limit: '1kb' }) + var test = request(server).post('/') + test.set('Content-Encoding', 'zstd') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('28b52ffd200e7100007b226e616d65223a22e8aeba227d', 'hex')) + test.expect(415, '[encoding.unsupported] unsupported content encoding "zstd"', done) + }) + it('should be case-insensitive', function (done) { var test = request(this.server).post('/') test.set('Content-Encoding', 'GZIP') diff --git a/test/raw.js b/test/raw.js index 033a97cb..d52bae83 100644 --- a/test/raw.js +++ b/test/raw.js @@ -3,10 +3,15 @@ var assert = require('node:assert') var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage var http = require('node:http') +const zlib = require('node:zlib') var request = require('supertest') var bodyParser = require('..') +const hasZstandardSupport = 'createZstdDecompress' in zlib +const zstandardit = hasZstandardSupport ? it : it.skip +const nozstandardit = !hasZstandardSupport ? it : it.skip + describe('bodyParser.raw()', function () { before(function () { this.server = createServer() @@ -458,6 +463,24 @@ describe('bodyParser.raw()', function () { test.expect(200, 'buf:6e616d653de8aeba', done) }) + zstandardit('should support zstandard encoding', function (done) { + const server = createServer({ experimentalZstd: true, limit: '10kb' }) + var test = request(server).post('/') + test.set('Content-Encoding', 'zstd') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('28b52ffd20084100006e616d653de8aeba', 'hex')) + test.expect(200, 'buf:6e616d653de8aeba', done) + }) + + nozstandardit('should throw 415 if there\'s no zstandard support', function (done) { + const server = createServer({ experimentalZstd: true, limit: '10kb' }) + var test = request(server).post('/') + test.set('Content-Encoding', 'zstd') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('28b52ffd20084100006e616d653de8aeba', 'hex')) + test.expect(415, '[encoding.unsupported] unsupported content encoding "zstd"', done) + }) + it('should be case-insensitive', function (done) { var test = request(this.server).post('/') test.set('Content-Encoding', 'GZIP') diff --git a/test/text.js b/test/text.js index 2b67aa95..209e0424 100644 --- a/test/text.js +++ b/test/text.js @@ -3,10 +3,15 @@ var assert = require('node:assert') var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage var http = require('node:http') +const zlib = require('node:zlib') var request = require('supertest') var bodyParser = require('..') +const hasZstandardSupport = 'createZstdDecompress' in zlib +const zstandardit = hasZstandardSupport ? it : it.skip +const nozstandardit = !hasZstandardSupport ? it : it.skip + describe('bodyParser.text()', function () { before(function () { this.server = createServer() @@ -528,6 +533,24 @@ describe('bodyParser.text()', function () { test.expect(200, '"name is 论"', done) }) + zstandardit('should support zstandard encoding', function (done) { + const server = createServer({ experimentalZstd: true, limit: '10kb' }) + var test = request(server).post('/') + test.set('Content-Encoding', 'zstd') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('28b52ffd200b5900006e616d6520697320e8aeba', 'hex')) + test.expect(200, '"name is 论"', done) + }) + + nozstandardit('should throw 415 if there\'s no zstandard support', function (done) { + const server = createServer({ experimentalZstd: true, limit: '10kb' }) + var test = request(server).post('/') + test.set('Content-Encoding', 'zstd') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('28b52ffd200b5900006e616d6520697320e8aeba', 'hex')) + test.expect(415, '[encoding.unsupported] unsupported content encoding "zstd"', done) + }) + it('should be case-insensitive', function (done) { var test = request(this.server).post('/') test.set('Content-Encoding', 'GZIP') diff --git a/test/urlencoded.js b/test/urlencoded.js index 9911bc5a..257f873c 100644 --- a/test/urlencoded.js +++ b/test/urlencoded.js @@ -3,10 +3,15 @@ var assert = require('node:assert') var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage var http = require('node:http') +const zlib = require('node:zlib') var request = require('supertest') var bodyParser = require('..') +const hasZstandardSupport = 'createZstdDecompress' in zlib +const zstandardit = hasZstandardSupport ? it : it.skip +const nozstandardit = !hasZstandardSupport ? it : it.skip + describe('bodyParser.urlencoded()', function () { before(function () { this.server = createServer() @@ -906,6 +911,24 @@ describe('bodyParser.urlencoded()', function () { test.expect(200, '{"name":"论"}', done) }) + zstandardit('should support zstandard encoding', function (done) { + const server = createServer({ experimentalZstd: true }) + var test = request(server).post('/') + test.set('Content-Encoding', 'zstd') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('28b52ffd20084100006e616d653de8aeba', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + nozstandardit('should throw 415 if there\'s no zstandard support', function (done) { + const server = createServer({ experimentalZstd: true }) + var test = request(server).post('/') + test.set('Content-Encoding', 'zstd') + test.set('Content-Type', 'application/x-www-form-urlencoded') + test.write(Buffer.from('28b52ffd20084100006e616d653de8aeba', 'hex')) + test.expect(415, '[encoding.unsupported] unsupported content encoding "zstd"', done) + }) + it('should be case-insensitive', function (done) { var test = request(this.server).post('/') test.set('Content-Encoding', 'GZIP')