From 19ff6bf239f86cccfa6411229fc36a284e55980d Mon Sep 17 00:00:00 2001 From: 43081j <43081j@users.noreply.github.com> Date: Sun, 11 Aug 2024 23:32:48 +0100 Subject: [PATCH] feat: require an extended body parser This changes `extended` to be settable to a custom body parser, or `false`. When `false`, the simple parser (`node:querystring`) will be used which supports the following syntax: - `foo=a&foo=b` becomes `{"foo":["a","b"]}` - `foo=bar` becomes `{"foo":"bar"}` Otherwise, it must be a function which parses the given string: ```ts { "extended": (bodyStr) => { const params = new URLSearchParams(bodyStr); return convertURLSearchParamsToObject(params); } } ``` --- lib/types/urlencoded.js | 53 +++++-------------- package.json | 1 - test/urlencoded.js | 109 ++++++---------------------------------- 3 files changed, 29 insertions(+), 134 deletions(-) diff --git a/lib/types/urlencoded.js b/lib/types/urlencoded.js index b2ca8f16..0262ff4f 100644 --- a/lib/types/urlencoded.js +++ b/lib/types/urlencoded.js @@ -26,12 +26,6 @@ var typeis = require('type-is') module.exports = urlencoded -/** - * Cache of parser modules. - */ - -var parsers = Object.create(null) - /** * Create a middleware to parse urlencoded bodies. * @@ -48,7 +42,6 @@ function urlencoded (options) { deprecate('undefined extended: provide extended option') } - var extended = opts.extended !== false var inflate = opts.inflate !== false var limit = typeof opts.limit !== 'number' ? bytes.parse(opts.limit || '100kb') @@ -61,8 +54,8 @@ function urlencoded (options) { } // create the appropriate query parser - var queryparse = extended - ? extendedparser(opts) + var queryparse = typeof opts.extended === 'function' + ? extendedparser(opts.extended, opts) : simpleparser(opts) // create the appropriate type checking function @@ -129,11 +122,10 @@ function urlencoded (options) { * @param {object} options */ -function extendedparser (options) { +function extendedparser (parse, options) { var parameterLimit = options.parameterLimit !== undefined ? options.parameterLimit : 1000 - var parse = parser('qs') if (isNaN(parameterLimit) || parameterLimit < 1) { throw new TypeError('option parameterLimit must be a positive number') @@ -153,15 +145,8 @@ function extendedparser (options) { }) } - var arrayLimit = Math.max(100, paramCount) - debug('parse extended urlencoding') - return parse(body, { - allowPrototypes: true, - arrayLimit: arrayLimit, - depth: Infinity, - parameterLimit: parameterLimit - }) + return parse(body) } } @@ -204,35 +189,23 @@ function parameterCount (body, limit) { return count } +let queryStringModule + /** - * Get parser for module name dynamically. + * Loads the querystring module lazily and caches it * - * @param {string} name * @return {function} * @api private */ -function parser (name) { - var mod = parsers[name] - - if (mod !== undefined) { - return mod.parse - } - - // this uses a switch for static require analysis - switch (name) { - case 'qs': - mod = require('qs') - break - case 'querystring': - mod = require('querystring') - break +function loadQueryStringModule () { + if (queryStringModule) { + return queryStringModule.parse } - // store to prevent invoking require() - parsers[name] = mod + queryStringModule = require('querystring') - return mod.parse + return queryStringModule.parse } /** @@ -245,7 +218,7 @@ function simpleparser (options) { var parameterLimit = options.parameterLimit !== undefined ? options.parameterLimit : 1000 - var parse = parser('querystring') + var parse = loadQueryStringModule() if (isNaN(parameterLimit) || parameterLimit < 1) { throw new TypeError('option parameterLimit must be a positive number') diff --git a/package.json b/package.json index ffabc735..fcbaf538 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.12.3", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" diff --git a/test/urlencoded.js b/test/urlencoded.js index 10b8c4d4..79974690 100644 --- a/test/urlencoded.js +++ b/test/urlencoded.js @@ -88,12 +88,20 @@ describe('bodyParser.urlencoded()', function () { .expect(200, '{"user":"tobi"}', done) }) - it('should parse extended syntax', function (done) { + it('should parse multiple key instances', function (done) { + request(this.server) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=Tobi&user=Loki') + .expect(200, '{"user":["Tobi","Loki"]}', done) + }) + + it('should parse not parse nested objects', function (done) { request(this.server) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user[name][first]=Tobi') - .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) + .expect(200, '{"user[name][first]":"Tobi"}', done) }) describe('with extended option', function () { @@ -119,103 +127,18 @@ describe('bodyParser.urlencoded()', function () { }) }) - describe('when true', function () { + describe('when set to a custom parse function', function () { before(function () { - this.server = createServer({ extended: true }) - }) - - it('should parse multiple key instances', function (done) { - request(this.server) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send('user=Tobi&user=Loki') - .expect(200, '{"user":["Tobi","Loki"]}', done) - }) - - it('should parse extended syntax', function (done) { - request(this.server) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send('user[name][first]=Tobi') - .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) - }) - - it('should parse parameters with dots', function (done) { - request(this.server) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send('user.name=Tobi') - .expect(200, '{"user.name":"Tobi"}', done) - }) - - it('should parse fully-encoded extended syntax', function (done) { - request(this.server) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send('user%5Bname%5D%5Bfirst%5D=Tobi') - .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done) - }) - - it('should parse array index notation', function (done) { - request(this.server) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send('foo[0]=bar&foo[1]=baz') - .expect(200, '{"foo":["bar","baz"]}', done) - }) - - it('should parse array index notation with large array', function (done) { - var str = 'f[0]=0' - - for (var i = 1; i < 500; i++) { - str += '&f[' + i + ']=' + i.toString(16) - } - - request(this.server) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(str) - .expect(function (res) { - var obj = JSON.parse(res.text) - assert.strictEqual(Object.keys(obj).length, 1) - assert.strictEqual(Array.isArray(obj.f), true) - assert.strictEqual(obj.f.length, 500) - }) - .expect(200, done) - }) - - it('should parse array of objects syntax', function (done) { - request(this.server) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .send('foo[0][bar]=baz&foo[0][fizz]=buzz&foo[]=done!') - .expect(200, '{"foo":[{"bar":"baz","fizz":"buzz"},"done!"]}', done) + const parser = () => ({ foo: 'bar' }) + this.server = createServer({ extended: parser }) }) - it('should parse deep object', function (done) { - var str = 'foo' - - for (var i = 0; i < 500; i++) { - str += '[p]' - } - - str += '=bar' - + it('should parse via custom parser', function (done) { request(this.server) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') - .send(str) - .expect(function (res) { - var obj = JSON.parse(res.text) - assert.strictEqual(Object.keys(obj).length, 1) - assert.strictEqual(typeof obj.foo, 'object') - - var depth = 0 - var ref = obj.foo - while ((ref = ref.p)) { depth++ } - assert.strictEqual(depth, 500) - }) - .expect(200, done) + .send('a=b') + .expect(200, '{"foo":"bar"}', done) }) }) })