Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: experimental zstandard support #586

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 20 additions & 9 deletions lib/read.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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) {
Expand Down Expand Up @@ -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']

Expand All @@ -161,19 +167,20 @@ function contentstream (req, debug, inflate) {
return req
}

var stream = createDecompressionStream(encoding, debug)
var stream = createDecompressionStream(encoding, experimentalZstd, debug)
req.pipe(stream)
return stream
}

/**
* 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')
Expand All @@ -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'
})
}

/**
Expand Down
4 changes: 3 additions & 1 deletion lib/types/json.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -142,7 +143,8 @@ function json (options) {
encoding: charset,
inflate: inflate,
limit: limit,
verify: verify
verify: verify,
experimentalZstd: experimentalZstd
})
}
}
Expand Down
4 changes: 3 additions & 1 deletion lib/types/raw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -85,7 +86,8 @@ function raw (options) {
encoding: null,
inflate: inflate,
limit: limit,
verify: verify
verify: verify,
experimentalZstd: experimentalZstd
})
}
}
Expand Down
4 changes: 3 additions & 1 deletion lib/types/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -90,7 +91,8 @@ function text (options) {
encoding: charset,
inflate: inflate,
limit: limit,
verify: verify
verify: verify,
experimentalZstd: experimentalZstd
})
}
}
Expand Down
4 changes: 3 additions & 1 deletion lib/types/urlencoded.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -117,7 +118,8 @@ function urlencoded (options) {
limit: limit,
verify: verify,
charsetSentinel: charsetSentinel,
interpretNumericEntities: interpretNumericEntities
interpretNumericEntities: interpretNumericEntities,
experimentalZstd: experimentalZstd
})
}
}
Expand Down
23 changes: 23 additions & 0 deletions test/json.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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')
Expand Down
23 changes: 23 additions & 0 deletions test/raw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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')
Expand Down
23 changes: 23 additions & 0 deletions test/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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')
Expand Down
23 changes: 23 additions & 0 deletions test/urlencoded.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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')
Expand Down