From c0b4eaa279a361cec55c3ae7248cb09764caf4c6 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Tue, 14 Apr 2026 23:24:06 -0700 Subject: [PATCH 1/2] fix: restore array parsing for `req.query` repeated keys (#7147) qs 6.14 enforces a default `arrayLimit` of 20, which caused the extended query parser to collapse query strings with more than 20 repeated values (e.g. `?ids=1&ids=2&...&ids=25`) into an object instead of an array. This regressed in 4.22.0 (with the bump to `qs@~6.14.1`) and broke existing consumers relying on the pre-6.14 behavior. Pass `arrayLimit: Infinity` to `qs.parse` in `parseExtendedQueryString` so repeated keys always produce arrays, matching the behavior of Express \<= 4.21.x. Added a regression test in `test/req.query.js` that sends 25 repeated values and asserts they round-trip as an array. --- lib/utils.js | 8 +++++++- test/req.query.js | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/utils.js b/lib/utils.js index 56e12b9b541..0b9d5491d2f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -287,7 +287,13 @@ function createETagGenerator (options) { function parseExtendedQueryString(str) { return qs.parse(str, { - allowPrototypes: true + allowPrototypes: true, + // qs >= 6.14 enforces a default arrayLimit of 20, which collapses larger + // repeated-key query strings into objects. Raise the limit to 1000 to + // restore the pre-6.14 behavior for common payloads while still bounding + // single-parameter array expansions like `arr[999999]=x`. Matches the + // parameter limit used elsewhere in the query parser. See #7147. + arrayLimit: 1000 }); } diff --git a/test/req.query.js b/test/req.query.js index 6fae592dccc..c7a793aab8f 100644 --- a/test/req.query.js +++ b/test/req.query.js @@ -38,6 +38,30 @@ describe('req', function(){ .get('/?user.name=tj') .expect(200, '{"user.name":"tj"}', done); }); + + it('should parse more than 20 repeated values as an array (#7147)', function (done) { + var app = createApp('extended'); + var ids = []; + var expected = []; + for (var i = 0; i < 25; i++) { + ids.push('ids=' + i); + expected.push(String(i)); + } + + request(app) + .get('/?' + ids.join('&')) + .expect(200, JSON.stringify({ ids: expected }), done); + }); + + it('should still reject array expansion beyond arrayLimit (#7147)', function (done) { + var app = createApp('extended'); + + // A single parameter with an index past arrayLimit (1000) must not + // allocate a sparse array; qs collapses it to an object. + request(app) + .get('/?arr[9999]=x') + .expect(200, '{"arr":{"9999":"x"}}', done); + }); }); describe('when "query parser" is simple', function () { From f0a137e9071a78d30bb9dbc7623451214465f684 Mon Sep 17 00:00:00 2001 From: Jon Church Date: Mon, 27 Apr 2026 16:59:24 -0400 Subject: [PATCH 2/2] chore: drop comment and issue refrences, drop sparse array test --- lib/utils.js | 5 ----- test/req.query.js | 12 +----------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 0b9d5491d2f..1dc7dcc27cd 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -288,11 +288,6 @@ function createETagGenerator (options) { function parseExtendedQueryString(str) { return qs.parse(str, { allowPrototypes: true, - // qs >= 6.14 enforces a default arrayLimit of 20, which collapses larger - // repeated-key query strings into objects. Raise the limit to 1000 to - // restore the pre-6.14 behavior for common payloads while still bounding - // single-parameter array expansions like `arr[999999]=x`. Matches the - // parameter limit used elsewhere in the query parser. See #7147. arrayLimit: 1000 }); } diff --git a/test/req.query.js b/test/req.query.js index c7a793aab8f..474b49bea9b 100644 --- a/test/req.query.js +++ b/test/req.query.js @@ -39,7 +39,7 @@ describe('req', function(){ .expect(200, '{"user.name":"tj"}', done); }); - it('should parse more than 20 repeated values as an array (#7147)', function (done) { + it('should parse more than 20 repeated values as an array', function (done) { var app = createApp('extended'); var ids = []; var expected = []; @@ -52,16 +52,6 @@ describe('req', function(){ .get('/?' + ids.join('&')) .expect(200, JSON.stringify({ ids: expected }), done); }); - - it('should still reject array expansion beyond arrayLimit (#7147)', function (done) { - var app = createApp('extended'); - - // A single parameter with an index past arrayLimit (1000) must not - // allocate a sparse array; qs collapses it to an object. - request(app) - .get('/?arr[9999]=x') - .expect(200, '{"arr":{"9999":"x"}}', done); - }); }); describe('when "query parser" is simple', function () {