'use strict' var assert = require('node:assert') var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage const { Buffer } = require('node:buffer'); var express = require('..') var request = require('supertest') describe('express.urlencoded()', function () { before(function () { this.app = createApp() }) it('should parse x-www-form-urlencoded', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=tobi') .expect(200, '{"user":"tobi"}', done) }) it('should 400 when invalid content-length', function (done) { var app = express() app.use(function (req, res, next) { req.headers['content-length'] = '20' // bad length next() }) app.use(express.urlencoded()) app.post('/', function (req, res) { res.json(req.body) }) request(app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('str=') .expect(400, /content length/, done) }) it('should handle Content-Length: 0', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .set('Content-Length', '0') .send('') .expect(200, '{}', done) }) it('should handle empty message-body', function (done) { request(createApp({ limit: '1kb' })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .set('Transfer-Encoding', 'chunked') .send('') .expect(200, '{}', done) }) it('should handle duplicated middleware', function (done) { var app = express() app.use(express.urlencoded()) app.use(express.urlencoded()) app.post('/', function (req, res) { res.json(req.body) }) request(app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=tobi') .expect(200, '{"user":"tobi"}', done) }) it('should not parse extended syntax', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user[name][first]=Tobi') .expect(200, '{"user[name][first]":"Tobi"}', done) }) describe('with extended option', function () { describe('when false', function () { before(function () { this.app = createApp({ extended: false }) }) it('should not parse extended syntax', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user[name][first]=Tobi') .expect(200, '{"user[name][first]":"Tobi"}', done) }) it('should parse multiple key instances', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=Tobi&user=Loki') .expect(200, '{"user":["Tobi","Loki"]}', done) }) }) describe('when true', function () { before(function () { this.app = createApp({ extended: true }) }) it('should parse multiple key instances', function (done) { request(this.app) .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.app) .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.app) .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.app) .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.app) .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.app) .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.app) .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) }) it('should parse deep object', function (done) { var str = 'foo' for (var i = 0; i < 32; i++) { str += '[p]' } str += '=bar' request(this.app) .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, 32) }) .expect(200, done) }) }) }) describe('with inflate option', function () { describe('when false', function () { before(function () { this.app = createApp({ inflate: false }) }) it('should not accept content-encoding', function (done) { var test = request(this.app).post('/') test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) test.expect(415, '[encoding.unsupported] content encoding unsupported', done) }) }) describe('when true', function () { before(function () { this.app = createApp({ inflate: true }) }) it('should accept content-encoding', function (done) { var test = request(this.app).post('/') test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) test.expect(200, '{"name":"论"}', done) }) }) }) describe('with limit option', function () { it('should 413 when over limit with Content-Length', function (done) { var buf = Buffer.alloc(1024, '.') request(createApp({ limit: '1kb' })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .set('Content-Length', '1028') .send('str=' + buf.toString()) .expect(413, done) }) it('should 413 when over limit with chunked encoding', function (done) { var app = createApp({ limit: '1kb' }) var buf = Buffer.alloc(1024, '.') var test = request(app).post('/') test.set('Content-Type', 'application/x-www-form-urlencoded') test.set('Transfer-Encoding', 'chunked') test.write('str=') test.write(buf.toString()) test.expect(413, done) }) it('should 413 when inflated body over limit', function (done) { var app = createApp({ limit: '1kb' }) var test = request(app).post('/') test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f9204040000', 'hex')) test.expect(413, done) }) it('should accept number of bytes', function (done) { var buf = Buffer.alloc(1024, '.') request(createApp({ limit: 1024 })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('str=' + buf.toString()) .expect(413, done) }) it('should not change when options altered', function (done) { var buf = Buffer.alloc(1024, '.') var options = { limit: '1kb' } var app = createApp(options) options.limit = '100kb' request(app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('str=' + buf.toString()) .expect(413, done) }) it('should not hang response', function (done) { var app = createApp({ limit: '8kb' }) var buf = Buffer.alloc(10240, '.') var test = request(app).post('/') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(buf) test.write(buf) test.write(buf) test.expect(413, done) }) it('should not error when inflating', function (done) { var app = createApp({ limit: '1kb' }) var test = request(app).post('/') test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f92040400', 'hex')) test.expect(413, done) }) }) describe('with parameterLimit option', function () { describe('with extended: false', function () { it('should reject 0', function () { assert.throws(createApp.bind(null, { extended: false, parameterLimit: 0 }), /TypeError: option parameterLimit must be a positive number/) }) it('should reject string', function () { assert.throws(createApp.bind(null, { extended: false, parameterLimit: 'beep' }), /TypeError: option parameterLimit must be a positive number/) }) it('should 413 if over limit', function (done) { request(createApp({ extended: false, parameterLimit: 10 })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(11)) .expect(413, '[parameters.too.many] too many parameters', done) }) it('should work when at the limit', function (done) { request(createApp({ extended: false, parameterLimit: 10 })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(10)) .expect(expectKeyCount(10)) .expect(200, done) }) it('should work if number is floating point', function (done) { request(createApp({ extended: false, parameterLimit: 10.1 })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(11)) .expect(413, /too many parameters/, done) }) it('should work with large limit', function (done) { request(createApp({ extended: false, parameterLimit: 5000 })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(5000)) .expect(expectKeyCount(5000)) .expect(200, done) }) it('should work with Infinity limit', function (done) { request(createApp({ extended: false, parameterLimit: Infinity })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(10000)) .expect(expectKeyCount(10000)) .expect(200, done) }) }) describe('with extended: true', function () { it('should reject 0', function () { assert.throws(createApp.bind(null, { extended: true, parameterLimit: 0 }), /TypeError: option parameterLimit must be a positive number/) }) it('should reject string', function () { assert.throws(createApp.bind(null, { extended: true, parameterLimit: 'beep' }), /TypeError: option parameterLimit must be a positive number/) }) it('should 413 if over limit', function (done) { request(createApp({ extended: true, parameterLimit: 10 })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(11)) .expect(413, '[parameters.too.many] too many parameters', done) }) it('should work when at the limit', function (done) { request(createApp({ extended: true, parameterLimit: 10 })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(10)) .expect(expectKeyCount(10)) .expect(200, done) }) it('should work if number is floating point', function (done) { request(createApp({ extended: true, parameterLimit: 10.1 })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(11)) .expect(413, /too many parameters/, done) }) it('should work with large limit', function (done) { request(createApp({ extended: true, parameterLimit: 5000 })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(5000)) .expect(expectKeyCount(5000)) .expect(200, done) }) it('should work with Infinity limit', function (done) { request(createApp({ extended: true, parameterLimit: Infinity })) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(10000)) .expect(expectKeyCount(10000)) .expect(200, done) }) }) }) describe('with type option', function () { describe('when "application/vnd.x-www-form-urlencoded"', function () { before(function () { this.app = createApp({ type: 'application/vnd.x-www-form-urlencoded' }) }) it('should parse for custom type', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/vnd.x-www-form-urlencoded') .send('user=tobi') .expect(200, '{"user":"tobi"}', done) }) it('should ignore standard type', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=tobi') .expect(200, '', done) }) }) describe('when ["urlencoded", "application/x-pairs"]', function () { before(function () { this.app = createApp({ type: ['urlencoded', 'application/x-pairs'] }) }) it('should parse "application/x-www-form-urlencoded"', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=tobi') .expect(200, '{"user":"tobi"}', done) }) it('should parse "application/x-pairs"', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-pairs') .send('user=tobi') .expect(200, '{"user":"tobi"}', done) }) it('should ignore application/x-foo', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-foo') .send('user=tobi') .expect(200, '', done) }) }) describe('when a function', function () { it('should parse when truthy value returned', function (done) { var app = createApp({ type: accept }) function accept (req) { return req.headers['content-type'] === 'application/vnd.something' } request(app) .post('/') .set('Content-Type', 'application/vnd.something') .send('user=tobi') .expect(200, '{"user":"tobi"}', done) }) it('should work without content-type', function (done) { var app = createApp({ type: accept }) function accept (req) { return true } var test = request(app).post('/') test.write('user=tobi') test.expect(200, '{"user":"tobi"}', done) }) it('should not invoke without a body', function (done) { var app = createApp({ type: accept }) function accept (req) { throw new Error('oops!') } request(app) .get('/') .expect(404, done) }) }) }) describe('with verify option', function () { it('should assert value if function', function () { assert.throws(createApp.bind(null, { verify: 'lol' }), /TypeError: option verify must be function/) }) it('should error from verify', function (done) { var app = createApp({ verify: function (req, res, buf) { if (buf[0] === 0x20) throw new Error('no leading space') } }) request(app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(' user=tobi') .expect(403, '[entity.verify.failed] no leading space', done) }) it('should allow custom codes', function (done) { var app = createApp({ verify: function (req, res, buf) { if (buf[0] !== 0x20) return var err = new Error('no leading space') err.status = 400 throw err } }) request(app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(' user=tobi') .expect(400, '[entity.verify.failed] no leading space', done) }) it('should allow custom type', function (done) { var app = createApp({ verify: function (req, res, buf) { if (buf[0] !== 0x20) return var err = new Error('no leading space') err.type = 'foo.bar' throw err } }) request(app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(' user=tobi') .expect(403, '[foo.bar] no leading space', done) }) it('should allow pass-through', function (done) { var app = createApp({ verify: function (req, res, buf) { if (buf[0] === 0x5b) throw new Error('no arrays') } }) request(app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=tobi') .expect(200, '{"user":"tobi"}', done) }) it('should 415 on unknown charset prior to verify', function (done) { var app = createApp({ verify: function (req, res, buf) { throw new Error('unexpected verify call') } }) var test = request(app).post('/') test.set('Content-Type', 'application/x-www-form-urlencoded; charset=x-bogus') test.write(Buffer.from('00000000', 'hex')) test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) }) }) describe('async local storage', function () { before(function () { var app = express() var store = { foo: 'bar' } app.use(function (req, res, next) { req.asyncLocalStorage = new AsyncLocalStorage() req.asyncLocalStorage.run(store, next) }) app.use(express.urlencoded()) app.use(function (req, res, next) { var local = req.asyncLocalStorage.getStore() if (local) { res.setHeader('x-store-foo', String(local.foo)) } next() }) app.use(function (err, req, res, next) { var local = req.asyncLocalStorage.getStore() if (local) { res.setHeader('x-store-foo', String(local.foo)) } res.status(err.status || 500) res.send('[' + err.type + '] ' + err.message) }) app.post('/', function (req, res) { res.json(req.body) }) this.app = app }) it('should persist store', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=tobi') .expect(200) .expect('x-store-foo', 'bar') .expect('{"user":"tobi"}') .end(done) }) it('should persist store when unmatched content-type', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/fizzbuzz') .send('buzz') .expect(200) .expect('x-store-foo', 'bar') .end(done) }) it('should persist store when inflated', function (done) { var test = request(this.app).post('/') test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) test.expect(200) test.expect('x-store-foo', 'bar') test.expect('{"name":"论"}') test.end(done) }) it('should persist store when inflate error', function (done) { var test = request(this.app).post('/') test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex')) test.expect(400) test.expect('x-store-foo', 'bar') test.end(done) }) it('should persist store when limit exceeded', function (done) { request(this.app) .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send('user=' + Buffer.alloc(1024 * 100, '.').toString()) .expect(413) .expect('x-store-foo', 'bar') .end(done) }) }) describe('charset', function () { before(function () { this.app = createApp() }) it('should parse utf-8', function (done) { var test = request(this.app).post('/') test.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8') test.write(Buffer.from('6e616d653de8aeba', 'hex')) test.expect(200, '{"name":"论"}', done) }) it('should parse when content-length != char length', function (done) { var test = request(this.app).post('/') test.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8') test.set('Content-Length', '7') test.write(Buffer.from('746573743dc3a5', 'hex')) test.expect(200, '{"test":"å"}', done) }) it('should default to utf-8', function (done) { var test = request(this.app).post('/') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('6e616d653de8aeba', 'hex')) test.expect(200, '{"name":"论"}', done) }) it('should fail on unknown charset', function (done) { var test = request(this.app).post('/') test.set('Content-Type', 'application/x-www-form-urlencoded; charset=koi8-r') test.write(Buffer.from('6e616d653dcec5d4', 'hex')) test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done) }) }) describe('encoding', function () { before(function () { this.app = createApp({ limit: '10kb' }) }) it('should parse without encoding', function (done) { var test = request(this.app).post('/') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('6e616d653de8aeba', 'hex')) test.expect(200, '{"name":"论"}', done) }) it('should support identity encoding', function (done) { var test = request(this.app).post('/') test.set('Content-Encoding', 'identity') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('6e616d653de8aeba', 'hex')) test.expect(200, '{"name":"论"}', done) }) it('should support gzip encoding', function (done) { var test = request(this.app).post('/') test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) test.expect(200, '{"name":"论"}', done) }) it('should support deflate encoding', function (done) { var test = request(this.app).post('/') test.set('Content-Encoding', 'deflate') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('789ccb4bcc4db57db16e17001068042f', 'hex')) test.expect(200, '{"name":"论"}', done) }) it('should be case-insensitive', function (done) { var test = request(this.app).post('/') test.set('Content-Encoding', 'GZIP') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) test.expect(200, '{"name":"论"}', done) }) it('should 415 on unknown encoding', function (done) { var test = request(this.app).post('/') test.set('Content-Encoding', 'nulls') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('000000000000', 'hex')) test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) }) }) }) function createManyParams (count) { var str = '' if (count === 0) { return str } str += '0=0' for (var i = 1; i < count; i++) { var n = i.toString(36) str += '&' + n + '=' + n } return str } function createApp (options) { var app = express() app.use(express.urlencoded(options)) app.use(function (err, req, res, next) { res.status(err.status || 500) res.send(String(req.headers['x-error-property'] ? err[req.headers['x-error-property']] : ('[' + err.type + '] ' + err.message))) }) app.post('/', function (req, res) { res.json(req.body) }) return app } function expectKeyCount (count) { return function (res) { assert.strictEqual(Object.keys(JSON.parse(res.text)).length, count) } }