Skip to content

Commit 7f6f349

Browse files
committed
feat: experimental zstandard support
1 parent eedea54 commit 7f6f349

File tree

10 files changed

+149
-15
lines changed

10 files changed

+149
-15
lines changed

README.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`
116116
where `buf` is a `Buffer` of the raw request body and `encoding` is the
117117
encoding of the request. The parsing can be aborted by throwing an error.
118118

119+
##### experimentalZstd
120+
121+
The `experimentalZstd` option enables support for automatic inflation of `zstd`
122+
(Zstandard) encoded request bodies. Requires Node.js 23.8.0 or later. Defaults
123+
to `false`.
124+
119125
### bodyParser.raw([options])
120126

121127
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)`
162168
where `buf` is a `Buffer` of the raw request body and `encoding` is the
163169
encoding of the request. The parsing can be aborted by throwing an error.
164170

171+
##### experimentalZstd
172+
173+
The `experimentalZstd` option enables support for automatic inflation of `zstd`
174+
(Zstandard) encoded request bodies. Requires Node.js 23.8.0 or later. Defaults
175+
to `false`.
176+
165177
### bodyParser.text([options])
166178

167179
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)`
212224
where `buf` is a `Buffer` of the raw request body and `encoding` is the
213225
encoding of the request. The parsing can be aborted by throwing an error.
214226

227+
##### experimentalZstd
228+
229+
The `experimentalZstd` option enables support for automatic inflation of `zstd`
230+
(Zstandard) encoded request bodies. Requires Node.js 23.8.0 or later. Defaults
231+
to `false`.
232+
215233
### bodyParser.urlencoded([options])
216234

217235
Returns middleware that only parses `urlencoded` bodies and only looks at
@@ -290,11 +308,16 @@ of `✓`. Defaults to `false`.
290308
Whether to decode numeric entities such as `☺` when parsing an iso-8859-1
291309
form. Defaults to `false`.
292310

293-
294-
#### depth
311+
##### depth
295312

296313
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.
297314

315+
##### experimentalZstd
316+
317+
The `experimentalZstd` option enables support for automatic inflation of `zstd`
318+
(Zstandard) encoded request bodies. Requires Node.js 23.8.0 or later. Defaults
319+
to `false`.
320+
298321
## Errors
299322

300323
The middlewares provided by this module create errors using the

lib/read.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ var zlib = require('node:zlib')
2323

2424
module.exports = read
2525

26+
/**
27+
* @const
28+
* whether current node version has zstandard support
29+
*/
30+
const hasZstandardSupport = 'createZstdDecompress' in zlib
31+
2632
/**
2733
* Read a request into a buffer and parse.
2834
*
@@ -48,7 +54,7 @@ function read (req, res, next, parse, debug, options) {
4854

4955
try {
5056
// get the content stream
51-
stream = contentstream(req, debug, opts.inflate)
57+
stream = contentstream(req, debug, opts.inflate, opts.experimentalZstd)
5258
length = stream.length
5359
stream.length = undefined
5460
} catch (err) {
@@ -143,7 +149,7 @@ function read (req, res, next, parse, debug, options) {
143149
* @api private
144150
*/
145151

146-
function contentstream (req, debug, inflate) {
152+
function contentstream (req, debug, inflate, experimentalZstd) {
147153
var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase()
148154
var length = req.headers['content-length']
149155

@@ -161,19 +167,20 @@ function contentstream (req, debug, inflate) {
161167
return req
162168
}
163169

164-
var stream = createDecompressionStream(encoding, debug)
170+
var stream = createDecompressionStream(encoding, experimentalZstd, debug)
165171
req.pipe(stream)
166172
return stream
167173
}
168174

169175
/**
170176
* Create a decompression stream for the given encoding.
171177
* @param {string} encoding
178+
* @param {boolean} experimentalZstd
172179
* @param {function} debug
173180
* @return {object}
174181
* @api private
175182
*/
176-
function createDecompressionStream (encoding, debug) {
183+
function createDecompressionStream (encoding, experimentalZstd, debug) {
177184
switch (encoding) {
178185
case 'deflate':
179186
debug('inflate body')
@@ -184,12 +191,16 @@ function createDecompressionStream (encoding, debug) {
184191
case 'br':
185192
debug('brotli decompress body')
186193
return zlib.createBrotliDecompress()
187-
default:
188-
throw createError(415, 'unsupported content encoding "' + encoding + '"', {
189-
encoding: encoding,
190-
type: 'encoding.unsupported'
191-
})
194+
case 'zstd':
195+
if (hasZstandardSupport && experimentalZstd === true) {
196+
debug('zstd decompress body')
197+
return zlib.createZstdDecompress()
198+
}
192199
}
200+
throw createError(415, 'unsupported content encoding "' + encoding + '"', {
201+
encoding: encoding,
202+
type: 'encoding.unsupported'
203+
})
193204
}
194205

195206
/**

lib/types/json.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function json (options) {
6262
var strict = opts.strict !== false
6363
var type = opts.type || 'application/json'
6464
var verify = opts.verify || false
65+
const experimentalZstd = opts.experimentalZstd === true || false
6566

6667
if (verify !== false && typeof verify !== 'function') {
6768
throw new TypeError('option verify must be function')
@@ -142,7 +143,8 @@ function json (options) {
142143
encoding: charset,
143144
inflate: inflate,
144145
limit: limit,
145-
verify: verify
146+
verify: verify,
147+
experimentalZstd: experimentalZstd
146148
})
147149
}
148150
}

lib/types/raw.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function raw (options) {
3939
: opts.limit
4040
var type = opts.type || 'application/octet-stream'
4141
var verify = opts.verify || false
42+
const experimentalZstd = opts.experimentalZstd === true || false
4243

4344
if (verify !== false && typeof verify !== 'function') {
4445
throw new TypeError('option verify must be function')
@@ -85,7 +86,8 @@ function raw (options) {
8586
encoding: null,
8687
inflate: inflate,
8788
limit: limit,
88-
verify: verify
89+
verify: verify,
90+
experimentalZstd: experimentalZstd
8991
})
9092
}
9193
}

lib/types/text.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function text (options) {
4141
: opts.limit
4242
var type = opts.type || 'text/plain'
4343
var verify = opts.verify || false
44+
const experimentalZstd = opts.experimentalZstd === true || false
4445

4546
if (verify !== false && typeof verify !== 'function') {
4647
throw new TypeError('option verify must be function')
@@ -90,7 +91,8 @@ function text (options) {
9091
encoding: charset,
9192
inflate: inflate,
9293
limit: limit,
93-
verify: verify
94+
verify: verify,
95+
experimentalZstd: experimentalZstd
9496
})
9597
}
9698
}

lib/types/urlencoded.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function urlencoded (options) {
4747
var verify = opts.verify || false
4848
var charsetSentinel = opts.charsetSentinel
4949
var interpretNumericEntities = opts.interpretNumericEntities
50+
const experimentalZstd = opts.experimentalZstd === true || false
5051

5152
if (verify !== false && typeof verify !== 'function') {
5253
throw new TypeError('option verify must be function')
@@ -117,7 +118,8 @@ function urlencoded (options) {
117118
limit: limit,
118119
verify: verify,
119120
charsetSentinel: charsetSentinel,
120-
interpretNumericEntities: interpretNumericEntities
121+
interpretNumericEntities: interpretNumericEntities,
122+
experimentalZstd: experimentalZstd
121123
})
122124
}
123125
}

test/json.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
var assert = require('node:assert')
44
var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage
55
var http = require('node:http')
6+
const zlib = require('node:zlib')
67
var request = require('supertest')
78

89
var bodyParser = require('..')
910

11+
const hasZstandardSupport = 'createZstdDecompress' in zlib
12+
const zstandardit = hasZstandardSupport ? it : it.skip
13+
const nozstandardit = !hasZstandardSupport ? it : it.skip
14+
1015
describe('bodyParser.json()', function () {
1116
it('should parse JSON', function (done) {
1217
request(createServer())
@@ -686,6 +691,24 @@ describe('bodyParser.json()', function () {
686691
test.expect(200, '{"name":"论"}', done)
687692
})
688693

694+
zstandardit('should support zstandard encoding', function (done) {
695+
const server = createServer({ experimentalZstd: true, limit: '1kb' })
696+
var test = request(server).post('/')
697+
test.set('Content-Encoding', 'zstd')
698+
test.set('Content-Type', 'application/json')
699+
test.write(Buffer.from('28b52ffd200e7100007b226e616d65223a22e8aeba227d', 'hex'))
700+
test.expect(200, '{"name":"论"}', done)
701+
})
702+
703+
nozstandardit('should throw 415 if there\'s no zstandard support', function (done) {
704+
const server = createServer({ experimentalZstd: true, limit: '1kb' })
705+
var test = request(server).post('/')
706+
test.set('Content-Encoding', 'zstd')
707+
test.set('Content-Type', 'application/json')
708+
test.write(Buffer.from('28b52ffd200e7100007b226e616d65223a22e8aeba227d', 'hex'))
709+
test.expect(415, '[encoding.unsupported] unsupported content encoding "zstd"', done)
710+
})
711+
689712
it('should be case-insensitive', function (done) {
690713
var test = request(this.server).post('/')
691714
test.set('Content-Encoding', 'GZIP')

test/raw.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
var assert = require('node:assert')
44
var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage
55
var http = require('node:http')
6+
const zlib = require('node:zlib')
67
var request = require('supertest')
78

89
var bodyParser = require('..')
910

11+
const hasZstandardSupport = 'createZstdDecompress' in zlib
12+
const zstandardit = hasZstandardSupport ? it : it.skip
13+
const nozstandardit = !hasZstandardSupport ? it : it.skip
14+
1015
describe('bodyParser.raw()', function () {
1116
before(function () {
1217
this.server = createServer()
@@ -458,6 +463,24 @@ describe('bodyParser.raw()', function () {
458463
test.expect(200, 'buf:6e616d653de8aeba', done)
459464
})
460465

466+
zstandardit('should support zstandard encoding', function (done) {
467+
const server = createServer({ experimentalZstd: true, limit: '10kb' })
468+
var test = request(server).post('/')
469+
test.set('Content-Encoding', 'zstd')
470+
test.set('Content-Type', 'application/octet-stream')
471+
test.write(Buffer.from('28b52ffd20084100006e616d653de8aeba', 'hex'))
472+
test.expect(200, 'buf:6e616d653de8aeba', done)
473+
})
474+
475+
nozstandardit('should throw 415 if there\'s no zstandard support', function (done) {
476+
const server = createServer({ experimentalZstd: true, limit: '10kb' })
477+
var test = request(server).post('/')
478+
test.set('Content-Encoding', 'zstd')
479+
test.set('Content-Type', 'application/octet-stream')
480+
test.write(Buffer.from('28b52ffd20084100006e616d653de8aeba', 'hex'))
481+
test.expect(415, '[encoding.unsupported] unsupported content encoding "zstd"', done)
482+
})
483+
461484
it('should be case-insensitive', function (done) {
462485
var test = request(this.server).post('/')
463486
test.set('Content-Encoding', 'GZIP')

test/text.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
var assert = require('node:assert')
44
var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage
55
var http = require('node:http')
6+
const zlib = require('node:zlib')
67
var request = require('supertest')
78

89
var bodyParser = require('..')
910

11+
const hasZstandardSupport = 'createZstdDecompress' in zlib
12+
const zstandardit = hasZstandardSupport ? it : it.skip
13+
const nozstandardit = !hasZstandardSupport ? it : it.skip
14+
1015
describe('bodyParser.text()', function () {
1116
before(function () {
1217
this.server = createServer()
@@ -528,6 +533,24 @@ describe('bodyParser.text()', function () {
528533
test.expect(200, '"name is 论"', done)
529534
})
530535

536+
zstandardit('should support zstandard encoding', function (done) {
537+
const server = createServer({ experimentalZstd: true, limit: '10kb' })
538+
var test = request(server).post('/')
539+
test.set('Content-Encoding', 'zstd')
540+
test.set('Content-Type', 'text/plain')
541+
test.write(Buffer.from('28b52ffd200b5900006e616d6520697320e8aeba', 'hex'))
542+
test.expect(200, '"name is 论"', done)
543+
})
544+
545+
nozstandardit('should throw 415 if there\'s no zstandard support', function (done) {
546+
const server = createServer({ experimentalZstd: true, limit: '10kb' })
547+
var test = request(server).post('/')
548+
test.set('Content-Encoding', 'zstd')
549+
test.set('Content-Type', 'text/plain')
550+
test.write(Buffer.from('28b52ffd200b5900006e616d6520697320e8aeba', 'hex'))
551+
test.expect(415, '[encoding.unsupported] unsupported content encoding "zstd"', done)
552+
})
553+
531554
it('should be case-insensitive', function (done) {
532555
var test = request(this.server).post('/')
533556
test.set('Content-Encoding', 'GZIP')

test/urlencoded.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
var assert = require('node:assert')
44
var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage
55
var http = require('node:http')
6+
const zlib = require('node:zlib')
67
var request = require('supertest')
78

89
var bodyParser = require('..')
910

11+
const hasZstandardSupport = 'createZstdDecompress' in zlib
12+
const zstandardit = hasZstandardSupport ? it : it.skip
13+
const nozstandardit = !hasZstandardSupport ? it : it.skip
14+
1015
describe('bodyParser.urlencoded()', function () {
1116
before(function () {
1217
this.server = createServer()
@@ -906,6 +911,24 @@ describe('bodyParser.urlencoded()', function () {
906911
test.expect(200, '{"name":"论"}', done)
907912
})
908913

914+
zstandardit('should support zstandard encoding', function (done) {
915+
const server = createServer({ experimentalZstd: true })
916+
var test = request(server).post('/')
917+
test.set('Content-Encoding', 'zstd')
918+
test.set('Content-Type', 'application/x-www-form-urlencoded')
919+
test.write(Buffer.from('28b52ffd20084100006e616d653de8aeba', 'hex'))
920+
test.expect(200, '{"name":"论"}', done)
921+
})
922+
923+
nozstandardit('should throw 415 if there\'s no zstandard support', function (done) {
924+
const server = createServer({ experimentalZstd: true })
925+
var test = request(server).post('/')
926+
test.set('Content-Encoding', 'zstd')
927+
test.set('Content-Type', 'application/x-www-form-urlencoded')
928+
test.write(Buffer.from('28b52ffd20084100006e616d653de8aeba', 'hex'))
929+
test.expect(415, '[encoding.unsupported] unsupported content encoding "zstd"', done)
930+
})
931+
909932
it('should be case-insensitive', function (done) {
910933
var test = request(this.server).post('/')
911934
test.set('Content-Encoding', 'GZIP')

0 commit comments

Comments
 (0)