diff --git a/History.md b/History.md index 2e542333d9..cba82afcbd 100644 --- a/History.md +++ b/History.md @@ -9,6 +9,16 @@ unreleased * Invoke `default` with same arguments as types in `res.format` * Support proper 205 responses using `res.send` * Use `http-errors` for `res.format` error + * deps: body-parser@1.20.0 + - Fix error message for json parse whitespace in `strict` + - Fix internal error when inflated body exceeds limit + - Prevent loss of async hooks context + - Prevent hanging when request already read + - deps: depd@2.0.0 + - deps: http-errors@2.0.0 + - deps: on-finished@2.4.1 + - deps: qs@6.10.3 + - deps: raw-body@2.5.1 * deps: depd@2.0.0 - Replace internal `eval` usage with `Function` constructor - Use instance methods on `process` to check for listeners diff --git a/package.json b/package.json index 71c110e5f2..ce6604bd7d 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.19.2", + "body-parser": "1.20.0", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.4.2", diff --git a/test/express.json.js b/test/express.json.js index 53a39565a9..a8cfebc41e 100644 --- a/test/express.json.js +++ b/test/express.json.js @@ -1,10 +1,15 @@ 'use strict' var assert = require('assert') +var asyncHooks = tryRequire('async_hooks') var Buffer = require('safe-buffer').Buffer var express = require('..') var request = require('supertest') +var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' + ? describe + : describe.skip + describe('express.json()', function () { it('should parse JSON', function (done) { request(createApp()) @@ -38,6 +43,14 @@ describe('express.json()', function () { .expect(200, '{}', done) }) + it('should 400 when only whitespace', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .send(' \n') + .expect(400, '[entity.parse.failed] ' + parseError(' '), done) + }) + it('should 400 when invalid content-length', function (done) { var app = express() @@ -59,6 +72,32 @@ describe('express.json()', function () { .expect(400, /content length/, done) }) + it('should 500 if stream not readable', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.on('end', next) + req.resume() + }) + + app.use(express.json()) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(500, '[stream.not.readable] stream is not readable', done) + }) + it('should handle duplicated middleware', function (done) { var app = express() @@ -86,7 +125,7 @@ describe('express.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('{:') - .expect(400, parseError('{:'), done) + .expect(400, '[entity.parse.failed] ' + parseError('{:'), done) }) it('should 400 for incomplete', function (done) { @@ -94,16 +133,7 @@ describe('express.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('{"user"') - .expect(400, parseError('{"user"'), done) - }) - - it('should error with type = "entity.parse.failed"', function (done) { - request(this.app) - .post('/') - .set('Content-Type', 'application/json') - .set('X-Error-Property', 'type') - .send(' {"user"') - .expect(400, 'entity.parse.failed', done) + .expect(400, '[entity.parse.failed] ' + parseError('{"user"'), done) }) it('should include original body on error object', function (done) { @@ -124,24 +154,13 @@ describe('express.json()', function () { .set('Content-Type', 'application/json') .set('Content-Length', '1034') .send(JSON.stringify({ str: buf.toString() })) - .expect(413, done) - }) - - it('should error with type = "entity.too.large"', function (done) { - var buf = Buffer.alloc(1024, '.') - request(createApp({ limit: '1kb' })) - .post('/') - .set('Content-Type', 'application/json') - .set('Content-Length', '1034') - .set('X-Error-Property', 'type') - .send(JSON.stringify({ str: buf.toString() })) - .expect(413, 'entity.too.large', done) + .expect(413, '[entity.too.large] request entity too large', done) }) it('should 413 when over limit with chunked encoding', function (done) { + var app = createApp({ limit: '1kb' }) var buf = Buffer.alloc(1024, '.') - var server = createApp({ limit: '1kb' }) - var test = request(server).post('/') + var test = request(app).post('/') test.set('Content-Type', 'application/json') test.set('Transfer-Encoding', 'chunked') test.write('{"str":') @@ -149,6 +168,15 @@ describe('express.json()', function () { 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/json') + test.write(Buffer.from('1f8b080000000000000aab562a2e2952b252d21b05a360148c58a0540b0066f7ce1e0a040000', 'hex')) + test.expect(413, done) + }) + it('should accept number of bytes', function (done) { var buf = Buffer.alloc(1024, '.') request(createApp({ limit: 1024 })) @@ -161,11 +189,11 @@ describe('express.json()', function () { it('should not change when options altered', function (done) { var buf = Buffer.alloc(1024, '.') var options = { limit: '1kb' } - var server = createApp(options) + var app = createApp(options) options.limit = '100kb' - request(server) + request(app) .post('/') .set('Content-Type', 'application/json') .send(JSON.stringify({ str: buf.toString() })) @@ -174,14 +202,23 @@ describe('express.json()', function () { it('should not hang response', function (done) { var buf = Buffer.alloc(10240, '.') - var server = createApp({ limit: '8kb' }) - var test = request(server).post('/') + var app = createApp({ limit: '8kb' }) + var test = request(app).post('/') test.set('Content-Type', 'application/json') 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/json') + test.write(Buffer.from('1f8b080000000000000aab562a2e2952b252d21b05a360148c58a0540b0066f7ce1e0a0400', 'hex')) + test.expect(413, done) + }) }) describe('with inflate option', function () { @@ -195,7 +232,7 @@ describe('express.json()', function () { test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'application/json') test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) - test.expect(415, 'content encoding unsupported', done) + test.expect(415, '[encoding.unsupported] content encoding unsupported', done) }) }) @@ -225,7 +262,7 @@ describe('express.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('true') - .expect(400, parseError('#rue').replace('#', 't'), done) + .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace('#', 't'), done) }) }) @@ -253,7 +290,7 @@ describe('express.json()', function () { .post('/') .set('Content-Type', 'application/json') .send('true') - .expect(400, parseError('#rue').replace('#', 't'), done) + .expect(400, '[entity.parse.failed] ' + parseError('#rue').replace('#', 't'), done) }) it('should not parse primitives with leading whitespaces', function (done) { @@ -261,7 +298,7 @@ describe('express.json()', function () { .post('/') .set('Content-Type', 'application/json') .send(' true') - .expect(400, parseError(' #rue').replace('#', 't'), done) + .expect(400, '[entity.parse.failed] ' + parseError(' #rue').replace('#', 't'), done) }) it('should allow leading whitespaces in JSON', function (done) { @@ -272,15 +309,6 @@ describe('express.json()', function () { .expect(200, '{"user":"tobi"}', done) }) - it('should error with type = "entity.parse.failed"', function (done) { - request(this.app) - .post('/') - .set('Content-Type', 'application/json') - .set('X-Error-Property', 'type') - .send('true') - .expect(400, 'entity.parse.failed', done) - }) - it('should include correct message in stack trace', function (done) { request(this.app) .post('/') @@ -397,65 +425,59 @@ describe('express.json()', function () { }) it('should error from verify', 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/json') - .send('["tobi"]') - .expect(403, 'no arrays', done) - }) - - it('should error with type = "entity.verify.failed"', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] === 0x5b) throw new Error('no arrays') - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } + }) request(app) .post('/') .set('Content-Type', 'application/json') - .set('X-Error-Property', 'type') .send('["tobi"]') - .expect(403, 'entity.verify.failed', done) + .expect(403, '[entity.verify.failed] no arrays', done) }) it('should allow custom codes', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] !== 0x5b) return - var err = new Error('no arrays') - err.status = 400 - throw err - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] !== 0x5b) return + var err = new Error('no arrays') + err.status = 400 + throw err + } + }) request(app) .post('/') .set('Content-Type', 'application/json') .send('["tobi"]') - .expect(400, 'no arrays', done) + .expect(400, '[entity.verify.failed] no arrays', done) }) it('should allow custom type', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] !== 0x5b) return - var err = new Error('no arrays') - err.type = 'foo.bar' - throw err - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] !== 0x5b) return + var err = new Error('no arrays') + err.type = 'foo.bar' + throw err + } + }) request(app) .post('/') .set('Content-Type', 'application/json') - .set('X-Error-Property', 'type') .send('["tobi"]') - .expect(403, 'foo.bar', done) + .expect(403, '[foo.bar] no arrays', done) }) it('should include original body on error object', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] === 0x5b) throw new Error('no arrays') - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } + }) request(app) .post('/') @@ -466,9 +488,11 @@ describe('express.json()', function () { }) it('should allow pass-through', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] === 0x5b) throw new Error('no arrays') - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } + }) request(app) .post('/') @@ -478,9 +502,11 @@ describe('express.json()', function () { }) it('should work with different charsets', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] === 0x5b) throw new Error('no arrays') - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } + }) var test = request(app).post('/') test.set('Content-Type', 'application/json; charset=utf-16') @@ -489,14 +515,120 @@ describe('express.json()', function () { }) 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 app = createApp({ + verify: function (req, res, buf) { + throw new Error('unexpected verify call') + } + }) var test = request(app).post('/') test.set('Content-Type', 'application/json; charset=x-bogus') test.write(Buffer.from('00000000', 'hex')) - test.expect(415, 'unsupported charset "X-BOGUS"', done) + test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) + }) + }) + + describeAsyncHooks('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.json()) + + 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 presist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('{"user":"tobi"}') + .end(done) + }) + + it('should presist 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') + .expect('{}') + .end(done) + }) + + it('should presist store when inflated', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200) + test.expect('x-store-foo', 'bar') + test.expect('{"name":"论"}') + test.end(done) + }) + + it('should presist store when inflate error', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56cc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(400) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should presist store when parse error', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":') + .expect(400) + .expect('x-store-foo', 'bar') + .end(done) + }) + + it('should presist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"' + Buffer.alloc(1024 * 100, '.').toString() + '"}') + .expect(413) + .expect('x-store-foo', 'bar') + .end(done) }) }) @@ -538,15 +670,7 @@ describe('express.json()', function () { var test = request(this.app).post('/') test.set('Content-Type', 'application/json; charset=koi8-r') test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex')) - test.expect(415, 'unsupported charset "KOI8-R"', done) - }) - - it('should error with type = "charset.unsupported"', function (done) { - var test = request(this.app).post('/') - test.set('Content-Type', 'application/json; charset=koi8-r') - test.set('X-Error-Property', 'type') - test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex')) - test.expect(415, 'charset.unsupported', done) + test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done) }) }) @@ -599,16 +723,7 @@ describe('express.json()', function () { test.set('Content-Encoding', 'nulls') test.set('Content-Type', 'application/json') test.write(Buffer.from('000000000000', 'hex')) - test.expect(415, 'unsupported content encoding "nulls"', done) - }) - - it('should error with type = "encoding.unsupported"', function (done) { - var test = request(this.app).post('/') - test.set('Content-Encoding', 'nulls') - test.set('Content-Type', 'application/json') - test.set('X-Error-Property', 'type') - test.write(Buffer.from('000000000000', 'hex')) - test.expect(415, 'encoding.unsupported', done) + test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) }) it('should 400 on malformed encoding', function (done) { @@ -639,7 +754,9 @@ function createApp (options) { app.use(function (err, req, res, next) { res.status(err.status || 500) - res.send(String(err[req.headers['x-error-property'] || 'message'])) + res.send(String(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.message))) }) app.post('/', function (req, res) { @@ -663,3 +780,11 @@ function shouldContainInBody (str) { 'expected \'' + res.text + '\' to contain \'' + str + '\'') } } + +function tryRequire (name) { + try { + return require(name) + } catch (e) { + return {} + } +} diff --git a/test/express.raw.js b/test/express.raw.js index cbd0736e7c..4aa62bb85b 100644 --- a/test/express.raw.js +++ b/test/express.raw.js @@ -1,10 +1,15 @@ 'use strict' var assert = require('assert') +var asyncHooks = tryRequire('async_hooks') var Buffer = require('safe-buffer').Buffer var express = require('..') var request = require('supertest') +var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' + ? describe + : describe.skip + describe('express.raw()', function () { before(function () { this.app = createApp() @@ -60,6 +65,36 @@ describe('express.raw()', function () { .expect(200, { buf: '' }, done) }) + it('should 500 if stream not readable', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.on('end', next) + req.resume() + }) + + app.use(express.raw()) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is tobi') + .expect(500, '[stream.not.readable] stream is not readable', done) + }) + it('should handle duplicated middleware', function (done) { var app = express() @@ -102,6 +137,15 @@ describe('express.raw()', function () { 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/octet-stream') + test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a14704040000', 'hex')) + test.expect(413, done) + }) + it('should accept number of bytes', function (done) { var buf = Buffer.alloc(1028, '.') var app = createApp({ limit: 1024 }) @@ -134,6 +178,15 @@ describe('express.raw()', function () { 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/octet-stream') + test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a147040400', 'hex')) + test.expect(413, done) + }) }) describe('with inflate option', function () { @@ -147,7 +200,7 @@ describe('express.raw()', function () { test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'application/octet-stream') test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) - test.expect(415, 'content encoding unsupported', done) + test.expect(415, '[encoding.unsupported] content encoding unsupported', done) }) }) @@ -263,34 +316,40 @@ describe('express.raw()', function () { }) it('should error from verify', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] === 0x00) throw new Error('no leading null') - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x00) throw new Error('no leading null') + } + }) var test = request(app).post('/') test.set('Content-Type', 'application/octet-stream') test.write(Buffer.from('000102', 'hex')) - test.expect(403, 'no leading null', done) + test.expect(403, '[entity.verify.failed] no leading null', done) }) it('should allow custom codes', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] !== 0x00) return - var err = new Error('no leading null') - err.status = 400 - throw err - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] !== 0x00) return + var err = new Error('no leading null') + err.status = 400 + throw err + } + }) var test = request(app).post('/') test.set('Content-Type', 'application/octet-stream') test.write(Buffer.from('000102', 'hex')) - test.expect(400, 'no leading null', done) + test.expect(400, '[entity.verify.failed] no leading null', done) }) it('should allow pass-through', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] === 0x00) throw new Error('no leading null') - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x00) throw new Error('no leading null') + } + }) var test = request(app).post('/') test.set('Content-Type', 'application/octet-stream') @@ -299,6 +358,104 @@ describe('express.raw()', function () { }) }) + describeAsyncHooks('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.raw()) + + 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) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + this.app = app + }) + + it('should presist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is tobi') + .expect(200) + .expect('x-store-foo', 'bar') + .expect({ buf: '746865207573657220697320746f6269' }) + .end(done) + }) + + it('should presist 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') + .expect('{}') + .end(done) + }) + + it('should presist store when inflated', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200) + test.expect('x-store-foo', 'bar') + test.expect({ buf: '6e616d653de8aeba' }) + test.end(done) + }) + + it('should presist store when inflate error', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex')) + test.expect(400) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should presist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is ' + Buffer.alloc(1024 * 100, '.').toString()) + .expect(413) + .expect('x-store-foo', 'bar') + .end(done) + }) + }) + describe('charset', function () { before(function () { this.app = createApp() @@ -356,12 +513,12 @@ describe('express.raw()', function () { test.expect(200, { buf: '6e616d653de8aeba' }, done) }) - it('should fail on unknown encoding', function (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/octet-stream') test.write(Buffer.from('000000000000', 'hex')) - test.expect(415, 'unsupported content encoding "nulls"', done) + test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) }) }) }) @@ -373,7 +530,9 @@ function createApp (options) { app.use(function (err, req, res, next) { res.status(err.status || 500) - res.send(String(err[req.headers['x-error-property'] || 'message'])) + res.send(String(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.message))) }) app.post('/', function (req, res) { @@ -386,3 +545,11 @@ function createApp (options) { return app } + +function tryRequire (name) { + try { + return require(name) + } catch (e) { + return {} + } +} diff --git a/test/express.text.js b/test/express.text.js index ebc12cd109..cb7750a525 100644 --- a/test/express.text.js +++ b/test/express.text.js @@ -1,10 +1,15 @@ 'use strict' var assert = require('assert') +var asyncHooks = tryRequire('async_hooks') var Buffer = require('safe-buffer').Buffer var express = require('..') var request = require('supertest') +var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' + ? describe + : describe.skip + describe('express.text()', function () { before(function () { this.app = createApp() @@ -56,6 +61,32 @@ describe('express.text()', function () { .expect(200, '""', done) }) + it('should 500 if stream not readable', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.on('end', next) + req.resume() + }) + + app.use(express.text()) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is tobi') + .expect(500, '[stream.not.readable] stream is not readable', done) + }) + it('should handle duplicated middleware', function (done) { var app = express() @@ -75,16 +106,16 @@ describe('express.text()', function () { describe('with defaultCharset option', function () { it('should change default charset', function (done) { - var app = createApp({ defaultCharset: 'koi8-r' }) - var test = request(app).post('/') + var server = createApp({ defaultCharset: 'koi8-r' }) + var test = request(server).post('/') test.set('Content-Type', 'text/plain') test.write(Buffer.from('6e616d6520697320cec5d4', 'hex')) test.expect(200, '"name is нет"', done) }) it('should honor content-type charset', function (done) { - var app = createApp({ defaultCharset: 'koi8-r' }) - var test = request(app).post('/') + var server = createApp({ defaultCharset: 'koi8-r' }) + var test = request(server).post('/') test.set('Content-Type', 'text/plain; charset=utf-8') test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) test.expect(200, '"name is 论"', done) @@ -103,8 +134,8 @@ describe('express.text()', function () { }) it('should 413 when over limit with chunked encoding', function (done) { - var buf = Buffer.alloc(1028, '.') var app = createApp({ limit: '1kb' }) + var buf = Buffer.alloc(1028, '.') var test = request(app).post('/') test.set('Content-Type', 'text/plain') test.set('Transfer-Encoding', 'chunked') @@ -112,6 +143,15 @@ describe('express.text()', function () { 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', 'text/plain') + test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a14704040000', 'hex')) + test.expect(413, done) + }) + it('should accept number of bytes', function (done) { var buf = Buffer.alloc(1028, '.') request(createApp({ limit: 1024 })) @@ -136,8 +176,8 @@ describe('express.text()', function () { }) it('should not hang response', function (done) { - var buf = Buffer.alloc(10240, '.') var app = createApp({ limit: '8kb' }) + var buf = Buffer.alloc(10240, '.') var test = request(app).post('/') test.set('Content-Type', 'text/plain') test.write(buf) @@ -145,6 +185,17 @@ describe('express.text()', function () { 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', 'text/plain') + test.write(Buffer.from('1f8b080000000000000ad3d31b05a360148c64000087e5a1470404', 'hex')) + setTimeout(function () { + test.expect(413, done) + }, 100) + }) }) describe('with inflate option', function () { @@ -158,7 +209,7 @@ describe('express.text()', function () { test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'text/plain') test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) - test.expect(415, 'content encoding unsupported', done) + test.expect(415, '[encoding.unsupported] content encoding unsupported', done) }) }) @@ -278,36 +329,42 @@ describe('express.text()', 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') - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x20) throw new Error('no leading space') + } + }) request(app) .post('/') .set('Content-Type', 'text/plain') .send(' user is tobi') - .expect(403, 'no leading space', done) + .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 - } }) + 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', 'text/plain') .send(' user is tobi') - .expect(400, 'no leading space', done) + .expect(400, '[entity.verify.failed] no leading space', done) }) it('should allow pass-through', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] === 0x20) throw new Error('no leading space') - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x20) throw new Error('no leading space') + } + }) request(app) .post('/') @@ -317,14 +374,110 @@ describe('express.text()', function () { }) 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 app = createApp({ + verify: function (req, res, buf) { + throw new Error('unexpected verify call') + } + }) var test = request(app).post('/') test.set('Content-Type', 'text/plain; charset=x-bogus') test.write(Buffer.from('00000000', 'hex')) - test.expect(415, 'unsupported charset "X-BOGUS"', done) + test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) + }) + }) + + describeAsyncHooks('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage() + req.asyncLocalStorage.run(store, next) + }) + + app.use(express.text()) + + 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 presist store', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is tobi') + .expect(200) + .expect('x-store-foo', 'bar') + .expect('"user is tobi"') + .end(done) + }) + + it('should presist 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') + .expect('{}') + .end(done) + }) + + it('should presist store when inflated', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b000000', 'hex')) + test.expect(200) + test.expect('x-store-foo', 'bar') + test.expect('"name is 论"') + test.end(done) + }) + + it('should presist store when inflate error', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'text/plain') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4d55c82c5678b16e170072b3e0200b0000', 'hex')) + test.expect(400) + test.expect('x-store-foo', 'bar') + test.end(done) + }) + + it('should presist store when limit exceeded', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'text/plain') + .send('user is ' + Buffer.alloc(1024 * 100, '.').toString()) + .expect(413) + .expect('x-store-foo', 'bar') + .end(done) }) }) @@ -366,7 +519,7 @@ describe('express.text()', function () { var test = request(this.app).post('/') test.set('Content-Type', 'text/plain; charset=x-bogus') test.write(Buffer.from('00000000', 'hex')) - test.expect(415, 'unsupported charset "X-BOGUS"', done) + test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) }) }) @@ -414,12 +567,12 @@ describe('express.text()', function () { test.expect(200, '"name is 论"', done) }) - it('should fail on unknown encoding', function (done) { + it('should 415 on unknown encoding', function (done) { var test = request(this.app).post('/') test.set('Content-Encoding', 'nulls') test.set('Content-Type', 'text/plain') test.write(Buffer.from('000000000000', 'hex')) - test.expect(415, 'unsupported content encoding "nulls"', done) + test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) }) }) }) @@ -431,7 +584,9 @@ function createApp (options) { app.use(function (err, req, res, next) { res.status(err.status || 500) - res.send(err.message) + res.send(String(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.message))) }) app.post('/', function (req, res) { @@ -440,3 +595,11 @@ function createApp (options) { return app } + +function tryRequire (name) { + try { + return require(name) + } catch (e) { + return {} + } +} diff --git a/test/express.urlencoded.js b/test/express.urlencoded.js index 340eb74316..e07432c86c 100644 --- a/test/express.urlencoded.js +++ b/test/express.urlencoded.js @@ -1,10 +1,15 @@ 'use strict' var assert = require('assert') +var asyncHooks = tryRequire('async_hooks') var Buffer = require('safe-buffer').Buffer var express = require('..') var request = require('supertest') +var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function' + ? describe + : describe.skip + describe('express.urlencoded()', function () { before(function () { this.app = createApp() @@ -57,6 +62,32 @@ describe('express.urlencoded()', function () { .expect(200, '{}', done) }) + it('should 500 if stream not readable', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.on('end', next) + req.resume() + }) + + app.use(express.urlencoded()) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send('[' + err.type + '] ' + err.message) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(500, '[stream.not.readable] stream is not readable', done) + }) + it('should handle duplicated middleware', function (done) { var app = express() @@ -217,7 +248,7 @@ describe('express.urlencoded()', function () { test.set('Content-Encoding', 'gzip') test.set('Content-Type', 'application/x-www-form-urlencoded') test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) - test.expect(415, 'content encoding unsupported', done) + test.expect(415, '[encoding.unsupported] content encoding unsupported', done) }) }) @@ -248,8 +279,8 @@ describe('express.urlencoded()', function () { }) it('should 413 when over limit with chunked encoding', function (done) { - var buf = Buffer.alloc(1024, '.') 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') @@ -258,6 +289,15 @@ describe('express.urlencoded()', function () { 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 })) @@ -282,8 +322,8 @@ describe('express.urlencoded()', function () { }) it('should not hang response', function (done) { - var buf = Buffer.alloc(10240, '.') 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) @@ -291,6 +331,15 @@ describe('express.urlencoded()', function () { 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 () { @@ -310,16 +359,7 @@ describe('express.urlencoded()', function () { .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(11)) - .expect(413, /too many parameters/, done) - }) - - it('should error with type = "parameters.too.many"', function (done) { - request(createApp({ extended: false, parameterLimit: 10 })) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .set('X-Error-Property', 'type') - .send(createManyParams(11)) - .expect(413, 'parameters.too.many', done) + .expect(413, '[parameters.too.many] too many parameters', done) }) it('should work when at the limit', function (done) { @@ -374,16 +414,7 @@ describe('express.urlencoded()', function () { .post('/') .set('Content-Type', 'application/x-www-form-urlencoded') .send(createManyParams(11)) - .expect(413, /too many parameters/, done) - }) - - it('should error with type = "parameters.too.many"', function (done) { - request(createApp({ extended: true, parameterLimit: 10 })) - .post('/') - .set('Content-Type', 'application/x-www-form-urlencoded') - .set('X-Error-Property', 'type') - .send(createManyParams(11)) - .expect(413, 'parameters.too.many', done) + .expect(413, '[parameters.too.many] too many parameters', done) }) it('should work when at the limit', function (done) { @@ -526,65 +557,59 @@ describe('express.urlencoded()', 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, 'no leading space', done) - }) - - it('should error with type = "entity.verify.failed"', function (done) { - var app = createApp({ verify: function (req, res, buf) { - if (buf[0] === 0x20) throw new Error('no leading space') - } }) + 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') - .set('X-Error-Property', 'type') .send(' user=tobi') - .expect(403, 'entity.verify.failed', done) + .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 - } }) + 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, 'no leading space', done) + .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 - } }) + 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') - .set('X-Error-Property', 'type') .send(' user=tobi') - .expect(403, 'foo.bar', done) + .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') - } }) + var app = createApp({ + verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } + }) request(app) .post('/') @@ -594,14 +619,110 @@ describe('express.urlencoded()', function () { }) 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 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, 'unsupported charset "X-BOGUS"', done) + test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done) + }) + }) + + describeAsyncHooks('async local storage', function () { + before(function () { + var app = express() + var store = { foo: 'bar' } + + app.use(function (req, res, next) { + req.asyncLocalStorage = new asyncHooks.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 presist 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 presist 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') + .expect('{}') + .end(done) + }) + + it('should presist 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 presist 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 presist 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) }) }) @@ -636,7 +757,7 @@ describe('express.urlencoded()', function () { 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, 'unsupported charset "KOI8-R"', done) + test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done) }) }) @@ -684,12 +805,12 @@ describe('express.urlencoded()', function () { test.expect(200, '{"name":"论"}', done) }) - it('should fail on unknown encoding', function (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, 'unsupported content encoding "nulls"', done) + test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done) }) }) }) @@ -718,7 +839,9 @@ function createApp (options) { app.use(function (err, req, res, next) { res.status(err.status || 500) - res.send(String(err[req.headers['x-error-property'] || 'message'])) + res.send(String(req.headers['x-error-property'] + ? err[req.headers['x-error-property']] + : ('[' + err.type + '] ' + err.message))) }) app.post('/', function (req, res) { @@ -733,3 +856,11 @@ function expectKeyCount (count) { assert.strictEqual(Object.keys(JSON.parse(res.text)).length, count) } } + +function tryRequire (name) { + try { + return require(name) + } catch (e) { + return {} + } +}