Skip to content

Commit

Permalink
feat: experimental zstandard support
Browse files Browse the repository at this point in the history
  • Loading branch information
Phillip9587 committed Feb 13, 2025
1 parent eedea54 commit 7f6f349
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 15 deletions.
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

0 comments on commit 7f6f349

Please sign in to comment.