Skip to content

Commit 39935cc

Browse files
committed
--wip-- [skip ci]
1 parent 2f27257 commit 39935cc

File tree

5 files changed

+89
-43
lines changed

5 files changed

+89
-43
lines changed

HISTORY.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ unreleased
22
=========================
33

44
* refactor: move common request validation to read function
5-
* deps:
5+
* deps:
66
* type-is@^2.0.1
77
* iconv-lite@^0.7.0
88
* Handle split surrogate pairs when encoding UTF-8
9-
* Avoid false positives in `encodingExists` by using prototype-less objects
9+
* Avoid false positives in `encodingExists` by using prototype-less objects
1010
* raw-body@^3.0.1
1111
* debug@^4.4.3
12+
* `urlencoded` can now be configured to use a query parsing function provided by the user
13+
* to restore the old behavior, use `urlencoded({ extended: qs.parse })`
14+
* the default `qs` module is now replaced with the `node:querystring` module
1215

1316
2.2.0 / 2025-03-27
1417
=========================
@@ -92,7 +95,7 @@ This incorporates all changes after 1.19.1 up to 1.20.2.
9295
9396
* add `depth` option to customize the depth level in the parser
9497
* IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`)
95-
98+
9699
1.20.2 / 2023-02-21
97100
===================
98101

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,16 @@ URL-encoded format, allowing for a JSON-like experience with URL-encoded. For
241241
more information, please [see the qs
242242
library](https://www.npmjs.org/package/qs#readme).
243243

244+
To use `node:querystring` with extended parsing, use `urlencoded({ extended: true })`. In
245+
this mode `extendedParsing: true` is implied.
246+
247+
To use `node:querystring` without extended parsing, use `urlencoded({ extended: false })`. In
248+
this mode `extendedParsing: false` is implied.
249+
250+
To use a custom url parser like `qs`, use `urlencoded({ extended: qs.parse })`. In
251+
this mode `extendedParsing: true` is implied, but can be overridden by using
252+
`urlencoded({ extended: qs.parse, extendedParsing: false })`.
253+
244254
Defaults to `false`.
245255

246256
##### inflate

lib/types/urlencoded.js

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
var createError = require('http-errors')
1616
var debug = require('debug')('body-parser:urlencoded')
1717
var read = require('../read')
18-
var qs = require('qs')
1918
var { normalizeOptions } = require('../utils')
2019

2120
/**
@@ -66,13 +65,16 @@ function urlencoded (options) {
6665
*/
6766

6867
function createQueryParser (options) {
69-
var extended = Boolean(options?.extended)
68+
var extendedOption = normalizeExtended(options?.extended, options?.extendedParsing)
69+
var extendedFunction = extendedOption.extendedFunction
70+
var extendedParsing = extendedOption.extendedParsing
71+
var allowPrototypes = Boolean(options?.allowPrototypes)
7072
var parameterLimit = options?.parameterLimit !== undefined
7173
? options?.parameterLimit
7274
: 1000
7375
var charsetSentinel = options?.charsetSentinel
7476
var interpretNumericEntities = options?.interpretNumericEntities
75-
var depth = extended ? (options?.depth !== undefined ? options?.depth : 32) : 0
77+
var depth = extendedParsing ? (options?.depth !== undefined ? options?.depth : 32) : 0
7678

7779
if (isNaN(parameterLimit) || parameterLimit < 1) {
7880
throw new TypeError('option parameterLimit must be a positive number')
@@ -96,12 +98,12 @@ function createQueryParser (options) {
9698
})
9799
}
98100

99-
var arrayLimit = extended ? Math.max(100, paramCount) : 0
101+
var arrayLimit = extendedParsing ? Math.max(100, paramCount) : 0
100102

101-
debug('parse ' + (extended ? 'extended ' : '') + 'urlencoding')
103+
debug('parse ' + (extendedParsing ? 'extended ' : '') + 'urlencoding')
102104
try {
103-
return qs.parse(body, {
104-
allowPrototypes: true,
105+
return extendedFunction(body, {
106+
allowPrototypes: allowPrototypes,
105107
arrayLimit: arrayLimit,
106108
depth: depth,
107109
charsetSentinel: charsetSentinel,
@@ -122,6 +124,33 @@ function createQueryParser (options) {
122124
}
123125
}
124126

127+
/**
128+
* @param {Boolean | Function} extended
129+
* @param {Boolean | null | undefined} extendedParsing
130+
* @api private
131+
*/
132+
133+
function normalizeExtended (extended, extendedParsing) {
134+
var normalizedExtendedParsing = Boolean(extendedParsing || extendedParsing === undefined || extendedParsing === null)
135+
if (typeof extended === 'function') {
136+
return { extendedFunction: extended, extendedParsing: normalizedExtendedParsing }
137+
}
138+
139+
var querystringParse = require('node:querystring').parse
140+
var extendedFunction = (str, options) =>
141+
querystringParse(str, undefined, undefined, { maxKeys: options.parameterLimit })
142+
143+
if (extended === true) {
144+
return { extendedFunction: extendedFunction, extendedParsing: true }
145+
}
146+
147+
if (extended === false || extended === undefined || extended === null) {
148+
return { extendedFunction: extendedFunction, extendedParsing: false }
149+
}
150+
151+
throw new TypeError('option extended must be a boolean or a function')
152+
}
153+
125154
/**
126155
* Count the number of parameters, stopping once limit reached
127156
*

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"http-errors": "^2.0.0",
2020
"iconv-lite": "^0.7.0",
2121
"on-finished": "^2.4.1",
22-
"qs": "^6.14.0",
2322
"raw-body": "^3.0.1",
2423
"type-is": "^2.0.1"
2524
},
@@ -33,6 +32,7 @@
3332
"eslint-plugin-standard": "^4.1.0",
3433
"mocha": "^11.1.0",
3534
"nyc": "^17.1.0",
35+
"qs": "^6.14.0",
3636
"supertest": "^7.0.0"
3737
},
3838
"files": [

test/urlencoded.js

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var assert = require('node:assert')
44
var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage
55
var http = require('node:http')
66
var request = require('supertest')
7+
var qs = require('qs')
78

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

@@ -43,11 +44,14 @@ describe('bodyParser.urlencoded()', function () {
4344
.expect(200, '{}', done)
4445
})
4546

46-
var extendedValues = [true, false]
47+
var extendedValues = [
48+
{ extended: qs.parse, extendedParsing: true },
49+
{ extended: qs.parse, extendedParsing: false }
50+
]
4751
extendedValues.forEach(function (extended) {
4852
describe('in ' + (extended ? 'extended' : 'simple') + ' mode', function () {
4953
it('should parse x-www-form-urlencoded with an explicit iso-8859-1 encoding', function (done) {
50-
var server = createServer({ extended: extended })
54+
var server = createServer({ ...extended })
5155
request(server)
5256
.post('/')
5357
.set('Content-Type', 'application/x-www-form-urlencoded; charset=iso-8859-1')
@@ -56,7 +60,7 @@ describe('bodyParser.urlencoded()', function () {
5660
})
5761

5862
it('should parse x-www-form-urlencoded with unspecified iso-8859-1 encoding when the defaultCharset is set to iso-8859-1', function (done) {
59-
var server = createServer({ defaultCharset: 'iso-8859-1', extended: extended })
63+
var server = createServer({ defaultCharset: 'iso-8859-1', ...extended })
6064
request(server)
6165
.post('/')
6266
.set('Content-Type', 'application/x-www-form-urlencoded')
@@ -65,7 +69,7 @@ describe('bodyParser.urlencoded()', function () {
6569
})
6670

6771
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) {
68-
var server = createServer({ charsetSentinel: true, extended: extended })
72+
var server = createServer({ charsetSentinel: true, ...extended })
6973
request(server)
7074
.post('/')
7175
.set('Content-Type', 'application/x-www-form-urlencoded')
@@ -74,7 +78,7 @@ describe('bodyParser.urlencoded()', function () {
7478
})
7579

7680
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) {
77-
var server = createServer({ charsetSentinel: true, extended: extended })
81+
var server = createServer({ charsetSentinel: true, ...extended })
7882
request(server)
7983
.post('/')
8084
.set('Content-Type', 'application/x-www-form-urlencoded')
@@ -83,7 +87,7 @@ describe('bodyParser.urlencoded()', function () {
8387
})
8488

8589
it('should not leave an empty string parameter when removing the utf8 sentinel from the start of the string', function (done) {
86-
var server = createServer({ charsetSentinel: true, extended: extended })
90+
var server = createServer({ charsetSentinel: true, ...extended })
8791
request(server)
8892
.post('/')
8993
.set('Content-Type', 'application/x-www-form-urlencoded')
@@ -92,7 +96,7 @@ describe('bodyParser.urlencoded()', function () {
9296
})
9397

9498
it('should not leave an empty string parameter when removing the utf8 sentinel from the middle of the string', function (done) {
95-
var server = createServer({ charsetSentinel: true, extended: extended })
99+
var server = createServer({ charsetSentinel: true, ...extended })
96100
request(server)
97101
.post('/')
98102
.set('Content-Type', 'application/x-www-form-urlencoded')
@@ -101,7 +105,7 @@ describe('bodyParser.urlencoded()', function () {
101105
})
102106

103107
it('should not leave an empty string parameter when removing the utf8 sentinel from the end of the string', function (done) {
104-
var server = createServer({ charsetSentinel: true, extended: extended })
108+
var server = createServer({ charsetSentinel: true, ...extended })
105109
request(server)
106110
.post('/')
107111
.set('Content-Type', 'application/x-www-form-urlencoded')
@@ -160,10 +164,10 @@ describe('bodyParser.urlencoded()', function () {
160164
.expect(200, '{"user[name][first]":"Tobi"}', done)
161165
})
162166

163-
describe('with extended option', function () {
164-
describe('when false', function () {
167+
describe('with extendedParsing option', function () {
168+
describe('with qs.parse when false', function () {
165169
before(function () {
166-
this.server = createServer({ extended: false })
170+
this.server = createServer({ extended: qs.parse, extendedParsing: false })
167171
})
168172

169173
it('should not parse extended syntax', function (done) {
@@ -183,9 +187,9 @@ describe('bodyParser.urlencoded()', function () {
183187
})
184188
})
185189

186-
describe('when true', function () {
190+
describe('with qs.parse when true', function () {
187191
before(function () {
188-
this.server = createServer({ extended: true })
192+
this.server = createServer({ extended: qs.parse })
189193
})
190194

191195
it('should parse multiple key instances', function (done) {
@@ -296,7 +300,7 @@ describe('bodyParser.urlencoded()', function () {
296300
})
297301

298302
it('should parse up to the specified depth', function (done) {
299-
this.server = createServer({ extended: true, depth: 10 })
303+
this.server = createServer({ extended: qs.parse, depth: 10 }) // TODO: test extended: true
300304
request(this.server)
301305
.post('/')
302306
.set('Content-Type', 'application/x-www-form-urlencoded')
@@ -305,7 +309,7 @@ describe('bodyParser.urlencoded()', function () {
305309
})
306310

307311
it('should not parse beyond the specified depth', function (done) {
308-
this.server = createServer({ extended: true, depth: 1 })
312+
this.server = createServer({ extended: qs.parse, depth: 1 })
309313
request(this.server)
310314
.post('/')
311315
.set('Content-Type', 'application/x-www-form-urlencoded')
@@ -316,7 +320,7 @@ describe('bodyParser.urlencoded()', function () {
316320

317321
describe('when default value', function () {
318322
before(function () {
319-
this.server = createServer({ extended: true })
323+
this.server = createServer({ extended: qs.parse })
320324
})
321325

322326
it('should parse deeply nested objects', function (done) {
@@ -462,27 +466,27 @@ describe('bodyParser.urlencoded()', function () {
462466
})
463467

464468
describe('with parameterLimit option', function () {
465-
describe('with extended: false', function () {
469+
describe('with extended: qs.parse, extendedParsing: false', function () {
466470
it('should reject 0', function () {
467-
assert.throws(createServer.bind(null, { extended: false, parameterLimit: 0 }),
471+
assert.throws(createServer.bind(null, { extended: qs.parse, extendedParsing: false, parameterLimit: 0 }),
468472
/TypeError: option parameterLimit must be a positive number/)
469473
})
470474

471475
it('should reject string', function () {
472-
assert.throws(createServer.bind(null, { extended: false, parameterLimit: 'beep' }),
476+
assert.throws(createServer.bind(null, { extended: qs.parse, extendedParsing: false, parameterLimit: 'beep' }),
473477
/TypeError: option parameterLimit must be a positive number/)
474478
})
475479

476480
it('should 413 if over limit', function (done) {
477-
request(createServer({ extended: false, parameterLimit: 10 }))
481+
request(createServer({ extended: qs.parse, extendedParsing: false, parameterLimit: 10 }))
478482
.post('/')
479483
.set('Content-Type', 'application/x-www-form-urlencoded')
480484
.send(createManyParams(11))
481485
.expect(413, '[parameters.too.many] too many parameters', done)
482486
})
483487

484488
it('should work when at the limit', function (done) {
485-
request(createServer({ extended: false, parameterLimit: 10 }))
489+
request(createServer({ extended: qs.parse, extendedParsing: false, parameterLimit: 10 }))
486490
.post('/')
487491
.set('Content-Type', 'application/x-www-form-urlencoded')
488492
.send(createManyParams(10))
@@ -491,15 +495,15 @@ describe('bodyParser.urlencoded()', function () {
491495
})
492496

493497
it('should work if number is floating point', function (done) {
494-
request(createServer({ extended: false, parameterLimit: 10.1 }))
498+
request(createServer({ extended: qs.parse, extendedParsing: false, parameterLimit: 10.1 }))
495499
.post('/')
496500
.set('Content-Type', 'application/x-www-form-urlencoded')
497501
.send(createManyParams(11))
498502
.expect(413, /too many parameters/, done)
499503
})
500504

501505
it('should work with large limit', function (done) {
502-
request(createServer({ extended: false, parameterLimit: 5000 }))
506+
request(createServer({ extended: qs.parse, extendedParsing: false, parameterLimit: 5000 }))
503507
.post('/')
504508
.set('Content-Type', 'application/x-www-form-urlencoded')
505509
.send(createManyParams(5000))
@@ -508,7 +512,7 @@ describe('bodyParser.urlencoded()', function () {
508512
})
509513

510514
it('should work with Infinity limit', function (done) {
511-
request(createServer({ extended: false, parameterLimit: Infinity }))
515+
request(createServer({ extended: qs.parse, extendedParsing: false, parameterLimit: Infinity }))
512516
.post('/')
513517
.set('Content-Type', 'application/x-www-form-urlencoded')
514518
.send(createManyParams(10000))
@@ -517,27 +521,27 @@ describe('bodyParser.urlencoded()', function () {
517521
})
518522
})
519523

520-
describe('with extended: true', function () {
524+
describe('with extended: qs.parse', function () {
521525
it('should reject 0', function () {
522-
assert.throws(createServer.bind(null, { extended: true, parameterLimit: 0 }),
526+
assert.throws(createServer.bind(null, { extended: qs.parse, parameterLimit: 0 }),
523527
/TypeError: option parameterLimit must be a positive number/)
524528
})
525529

526530
it('should reject string', function () {
527-
assert.throws(createServer.bind(null, { extended: true, parameterLimit: 'beep' }),
531+
assert.throws(createServer.bind(null, { extended: qs.parse, parameterLimit: 'beep' }),
528532
/TypeError: option parameterLimit must be a positive number/)
529533
})
530534

531535
it('should 413 if over limit', function (done) {
532-
request(createServer({ extended: true, parameterLimit: 10 }))
536+
request(createServer({ extended: qs.parse, parameterLimit: 10 }))
533537
.post('/')
534538
.set('Content-Type', 'application/x-www-form-urlencoded')
535539
.send(createManyParams(11))
536540
.expect(413, '[parameters.too.many] too many parameters', done)
537541
})
538542

539543
it('should work when at the limit', function (done) {
540-
request(createServer({ extended: true, parameterLimit: 10 }))
544+
request(createServer({ extended: qs.parse, parameterLimit: 10 }))
541545
.post('/')
542546
.set('Content-Type', 'application/x-www-form-urlencoded')
543547
.send(createManyParams(10))
@@ -546,15 +550,15 @@ describe('bodyParser.urlencoded()', function () {
546550
})
547551

548552
it('should work if number is floating point', function (done) {
549-
request(createServer({ extended: true, parameterLimit: 10.1 }))
553+
request(createServer({ extended: qs.parse, parameterLimit: 10.1 }))
550554
.post('/')
551555
.set('Content-Type', 'application/x-www-form-urlencoded')
552556
.send(createManyParams(11))
553557
.expect(413, /too many parameters/, done)
554558
})
555559

556560
it('should work with large limit', function (done) {
557-
request(createServer({ extended: true, parameterLimit: 5000 }))
561+
request(createServer({ extended: qs.parse, parameterLimit: 5000 }))
558562
.post('/')
559563
.set('Content-Type', 'application/x-www-form-urlencoded')
560564
.send(createManyParams(5000))
@@ -563,7 +567,7 @@ describe('bodyParser.urlencoded()', function () {
563567
})
564568

565569
it('should work with Infinity limit', function (done) {
566-
request(createServer({ extended: true, parameterLimit: Infinity }))
570+
request(createServer({ extended: qs.parse, parameterLimit: Infinity }))
567571
.post('/')
568572
.set('Content-Type', 'application/x-www-form-urlencoded')
569573
.send(createManyParams(10000))

0 commit comments

Comments
 (0)