diff --git a/lib/utils.js b/lib/utils.js index 4f21e7ef1e3..6cd7e7d58d3 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -266,6 +266,11 @@ function createETagGenerator (options) { function parseExtendedQueryString(str) { return qs.parse(str, { - allowPrototypes: true + allowPrototypes: true, + // fix(#7147): qs defaults arrayLimit to 20, causing arrays with more than + // 20 items to be returned as a plain object with numeric string keys instead + // of an array. Setting Infinity removes that cap while the existing + // parameterLimit (default: 1000) already bounds total query string size. + arrayLimit: Infinity }); } diff --git a/test/req.query.js b/test/req.query.js index c0d3c8376e9..a388a412dce 100644 --- a/test/req.query.js +++ b/test/req.query.js @@ -104,3 +104,63 @@ function createApp(setting) { return app; } + +// regression test for issue #7147 +// req.query was returning a plain object (not an array) when a repeated +// query param had more than 20 values — caused by qs defaulting arrayLimit to 20 +describe('regression: issue #7147 — arrays with more than 20 values', function () { + it('should parse 21 repeated query params as an array, not an object', function (done) { + var app = createApp('extended'); + + // Build ?ids=0&ids=1&...&ids=20 (21 values) + var query = Array.from({ length: 21 }, function (_, i) { return 'ids=' + i; }).join('&'); + + request(app) + .get('/?' + query) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + var parsed = JSON.parse(res.text); + assert.ok(Array.isArray(parsed.ids), + 'req.query.ids should be an Array, got: ' + JSON.stringify(parsed.ids)); + assert.strictEqual(parsed.ids.length, 21); + done(); + }); + }); + + it('should parse 100 repeated query params as an array', function (done) { + var app = createApp('extended'); + + var query = Array.from({ length: 100 }, function (_, i) { return 'ids=' + i; }).join('&'); + + request(app) + .get('/?' + query) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + var parsed = JSON.parse(res.text); + assert.ok(Array.isArray(parsed.ids), + 'req.query.ids should be an Array, got: ' + typeof parsed.ids); + assert.strictEqual(parsed.ids.length, 100); + done(); + }); + }); + + it('should still parse 20 or fewer repeated params as an array', function (done) { + var app = createApp('extended'); + + var query = Array.from({ length: 20 }, function (_, i) { return 'ids=' + i; }).join('&'); + + request(app) + .get('/?' + query) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + var parsed = JSON.parse(res.text); + assert.ok(Array.isArray(parsed.ids), + 'req.query.ids should be an Array for 20 items'); + assert.strictEqual(parsed.ids.length, 20); + done(); + }); + }); +}); diff --git a/test/res.type.js b/test/res.type.js index 09285af3914..e438956313a 100644 --- a/test/res.type.js +++ b/test/res.type.js @@ -42,5 +42,74 @@ describe('res', function(){ .get('/') .expect('Content-Type', 'application/vnd.amazon.ebook', done); }) + + describe('edge cases', function(){ + it('should handle empty string gracefully', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('').end('test'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/octet-stream') + .end(done); + }) + + it('should handle file extension with dots', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('.json').end('{"test": true}'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .end(done); + }) + + it('should handle multiple file extensions', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('file.tar.gz').end('compressed'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/gzip') + .end(done); + }) + + it('should handle uppercase extensions', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('FILE.JSON').end('{"test": true}'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .end(done); + }) + + it('should handle extension with special characters', function(done){ + var app = express(); + + app.use(function(req, res){ + res.type('file@test.json').end('{"test": true}'); + }); + + request(app) + .get('/') + .expect('Content-Type', 'application/json; charset=utf-8') + .end(done); + }) + }) }) }) + +