From eb4c930d5fb79e55c53b25d6e8a8a5cd15fc5554 Mon Sep 17 00:00:00 2001
From: Kris Kalavantavanich <kkalavantavanich@gmail.com>
Date: Sun, 5 Dec 2021 15:14:40 +0700
Subject: [PATCH 01/31] build: support Node.js 15.x

---
 .github/workflows/ci.yml | 4 ++++
 appveyor.yml             | 1 +
 2 files changed, 5 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d6e1b168ec..8e60f419c6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,6 +27,7 @@ jobs:
         - Node.js 12.x
         - Node.js 13.x
         - Node.js 14.x
+        - Node.js 15.x
 
         include:
         - name: Node.js 0.10
@@ -90,6 +91,9 @@ jobs:
         - name: Node.js 14.x
           node-version: "14.19"
 
+        - name: Node.js 15.x
+          node-version: "15.14"
+
     steps:
     - uses: actions/checkout@v2
 
diff --git a/appveyor.yml b/appveyor.yml
index db54a3fdb0..fc867adee7 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -16,6 +16,7 @@ environment:
     - nodejs_version: "12.22"
     - nodejs_version: "13.14"
     - nodejs_version: "14.19"
+    - nodejs_version: "15.14"
 cache:
   - node_modules
 install:

From 8bf072039100cd264be920f06635fe77f083c751 Mon Sep 17 00:00:00 2001
From: Kris Kalavantavanich <kkalavantavanich@gmail.com>
Date: Sun, 5 Dec 2021 15:15:26 +0700
Subject: [PATCH 02/31] build: support Node.js 16.x

---
 .github/workflows/ci.yml | 4 ++++
 appveyor.yml             | 1 +
 2 files changed, 5 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8e60f419c6..7b153d1b43 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,6 +28,7 @@ jobs:
         - Node.js 13.x
         - Node.js 14.x
         - Node.js 15.x
+        - Node.js 16.x
 
         include:
         - name: Node.js 0.10
@@ -94,6 +95,9 @@ jobs:
         - name: Node.js 15.x
           node-version: "15.14"
 
+        - name: Node.js 16.x
+          node-version: "16.14"
+
     steps:
     - uses: actions/checkout@v2
 
diff --git a/appveyor.yml b/appveyor.yml
index fc867adee7..2a2507b411 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -17,6 +17,7 @@ environment:
     - nodejs_version: "13.14"
     - nodejs_version: "14.19"
     - nodejs_version: "15.14"
+    - nodejs_version: "16.14"
 cache:
   - node_modules
 install:

From 87279c08aa46d178fe6f2e235d2345f17d6b5a37 Mon Sep 17 00:00:00 2001
From: "Tito D. Kesumo Siregar" <tito.siregar@bukalapak.com>
Date: Thu, 20 May 2021 11:23:33 +0700
Subject: [PATCH 03/31] Support proper 205 responses using res.send

closes #4592
closes #4596
---
 History.md       |  5 +++++
 lib/response.js  |  7 +++++++
 test/res.send.js | 16 ++++++++++++++++
 3 files changed, 28 insertions(+)

diff --git a/History.md b/History.md
index 9f3f876512..fbe0126907 100644
--- a/History.md
+++ b/History.md
@@ -1,3 +1,8 @@
+unreleased
+==========
+
+  * Support proper 205 responses using `res.send`
+
 4.17.3 / 2022-02-16
 ===================
 
diff --git a/lib/response.js b/lib/response.js
index ccf8d91b2c..9cf3d52be5 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -213,6 +213,13 @@ res.send = function send(body) {
     chunk = '';
   }
 
+  // alter headers for 205
+  if (this.statusCode === 205) {
+    this.set('Content-Length', '0')
+    this.removeHeader('Transfer-Encoding')
+    chunk = ''
+  }
+
   if (req.method === 'HEAD') {
     // skip body for HEAD
     this.end();
diff --git a/test/res.send.js b/test/res.send.js
index 6ba5542288..c92568db6a 100644
--- a/test/res.send.js
+++ b/test/res.send.js
@@ -283,6 +283,22 @@ describe('res', function(){
     })
   })
 
+  describe('when .statusCode is 205', function () {
+    it('should strip Transfer-Encoding field and body, set Content-Length', function (done) {
+      var app = express()
+
+      app.use(function (req, res) {
+        res.status(205).set('Transfer-Encoding', 'chunked').send('foo')
+      })
+
+      request(app)
+        .get('/')
+        .expect(utils.shouldNotHaveHeader('Transfer-Encoding'))
+        .expect('Content-Length', '0')
+        .expect(205, '', done)
+    })
+  })
+
   describe('when .statusCode is 304', function(){
     it('should strip Content-* fields, Transfer-Encoding field, and body', function(done){
       var app = express();

From c17fe058613dc7dfb7779fbe68a9738a108fe408 Mon Sep 17 00:00:00 2001
From: Evan Hahn <me@evanhahn.com>
Date: Wed, 2 Feb 2022 19:13:54 -0600
Subject: [PATCH 04/31] Ignore Object.prototype values in settings through
 app.set/app.get

closes #4802
closes #4803
---
 History.md         |  1 +
 lib/application.js | 19 ++++++++++++++++++-
 test/config.js     | 44 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/History.md b/History.md
index fbe0126907..97345bb411 100644
--- a/History.md
+++ b/History.md
@@ -1,6 +1,7 @@
 unreleased
 ==========
 
+  * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
   * Support proper 205 responses using `res.send`
 
 4.17.3 / 2022-02-16
diff --git a/lib/application.js b/lib/application.js
index e65ba58895..ebb30b51b3 100644
--- a/lib/application.js
+++ b/lib/application.js
@@ -29,6 +29,13 @@ var flatten = require('array-flatten');
 var merge = require('utils-merge');
 var resolve = require('path').resolve;
 var setPrototypeOf = require('setprototypeof')
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var hasOwnProperty = Object.prototype.hasOwnProperty
 var slice = Array.prototype.slice;
 
 /**
@@ -352,7 +359,17 @@ app.param = function param(name, fn) {
 app.set = function set(setting, val) {
   if (arguments.length === 1) {
     // app.get(setting)
-    return this.settings[setting];
+    var settings = this.settings
+
+    while (settings && settings !== Object.prototype) {
+      if (hasOwnProperty.call(settings, setting)) {
+        return settings[setting]
+      }
+
+      settings = Object.getPrototypeOf(settings)
+    }
+
+    return undefined
   }
 
   debug('set "%s" to %o', setting, val);
diff --git a/test/config.js b/test/config.js
index 8386a4471c..b04367fdbf 100644
--- a/test/config.js
+++ b/test/config.js
@@ -11,6 +11,12 @@ describe('config', function () {
       assert.equal(app.get('foo'), 'bar');
     })
 
+    it('should set prototype values', function () {
+      var app = express()
+      app.set('hasOwnProperty', 42)
+      assert.strictEqual(app.get('hasOwnProperty'), 42)
+    })
+
     it('should return the app', function () {
       var app = express();
       assert.equal(app.set('foo', 'bar'), app);
@@ -21,6 +27,17 @@ describe('config', function () {
       assert.equal(app.set('foo', undefined), app);
     })
 
+    it('should return set value', function () {
+      var app = express()
+      app.set('foo', 'bar')
+      assert.strictEqual(app.set('foo'), 'bar')
+    })
+
+    it('should return undefined for prototype values', function () {
+      var app = express()
+      assert.strictEqual(app.set('hasOwnProperty'), undefined)
+    })
+
     describe('"etag"', function(){
       it('should throw on bad value', function(){
         var app = express();
@@ -51,6 +68,11 @@ describe('config', function () {
       assert.strictEqual(app.get('foo'), undefined);
     })
 
+    it('should return undefined for prototype values', function () {
+      var app = express()
+      assert.strictEqual(app.get('hasOwnProperty'), undefined)
+    })
+
     it('should otherwise return the value', function(){
       var app = express();
       app.set('foo', 'bar');
@@ -125,6 +147,12 @@ describe('config', function () {
       assert.equal(app.enable('tobi'), app);
       assert.strictEqual(app.get('tobi'), true);
     })
+
+    it('should set prototype values', function () {
+      var app = express()
+      app.enable('hasOwnProperty')
+      assert.strictEqual(app.get('hasOwnProperty'), true)
+    })
   })
 
   describe('.disable()', function(){
@@ -133,6 +161,12 @@ describe('config', function () {
       assert.equal(app.disable('tobi'), app);
       assert.strictEqual(app.get('tobi'), false);
     })
+
+    it('should set prototype values', function () {
+      var app = express()
+      app.disable('hasOwnProperty')
+      assert.strictEqual(app.get('hasOwnProperty'), false)
+    })
   })
 
   describe('.enabled()', function(){
@@ -146,6 +180,11 @@ describe('config', function () {
       app.set('foo', 'bar');
       assert.strictEqual(app.enabled('foo'), true);
     })
+
+    it('should default to false for prototype values', function () {
+      var app = express()
+      assert.strictEqual(app.enabled('hasOwnProperty'), false)
+    })
   })
 
   describe('.disabled()', function(){
@@ -159,5 +198,10 @@ describe('config', function () {
       app.set('foo', 'bar');
       assert.strictEqual(app.disabled('foo'), false);
     })
+
+    it('should default to true for prototype values', function () {
+      var app = express()
+      assert.strictEqual(app.disabled('hasOwnProperty'), true)
+    })
   })
 })

From 4847d0efa123fae8f12a2c1b88f7e1a87a5f145a Mon Sep 17 00:00:00 2001
From: Jon Church <me@jonchurch.com>
Date: Sat, 21 Mar 2020 05:04:16 -0400
Subject: [PATCH 05/31] Deprecate string and non-integer arguments to
 res.status

closes #4223
---
 History.md         |   1 +
 lib/response.js    |   3 +
 test/res.status.js | 205 ++++++++++++++++++++++++++++++++++++++++++---
 3 files changed, 197 insertions(+), 12 deletions(-)

diff --git a/History.md b/History.md
index 97345bb411..2e549136a5 100644
--- a/History.md
+++ b/History.md
@@ -1,6 +1,7 @@
 unreleased
 ==========
 
+  * Deprecate string and non-integer arguments to `res.status`
   * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
   * Support proper 205 responses using `res.send`
 
diff --git a/lib/response.js b/lib/response.js
index 9cf3d52be5..7a9564d262 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -64,6 +64,9 @@ var charsetRegExp = /;\s*charset\s*=/;
  */
 
 res.status = function status(code) {
+  if ((typeof code === 'string' || Math.floor(code) !== code) && code > 99 && code < 1000) {
+    deprecate('res.status(' + JSON.stringify(code) + '): use res.status(' + Math.floor(code) + ') instead')
+  }
   this.statusCode = code;
   return this;
 };
diff --git a/test/res.status.js b/test/res.status.js
index e0abc73c4c..1fe08344ea 100644
--- a/test/res.status.js
+++ b/test/res.status.js
@@ -1,21 +1,202 @@
 'use strict'
 
 var express = require('../')
-  , request = require('supertest');
+var request = require('supertest')
 
-describe('res', function(){
-  describe('.status(code)', function(){
-    it('should set the response .statusCode', function(done){
-      var app = express();
+var isIoJs = process.release
+  ? process.release.name === 'io.js'
+  : ['v1.', 'v2.', 'v3.'].indexOf(process.version.slice(0, 3)) !== -1
 
-      app.use(function(req, res){
-        res.status(201).end('Created');
-      });
+describe('res', function () {
+  describe('.status(code)', function () {
+    describe('when "code" is undefined', function () {
+      it('should raise error for invalid status code', function (done) {
+        var app = express()
 
-      request(app)
-      .get('/')
-      .expect('Created')
-      .expect(201, done);
+        app.use(function (req, res) {
+          res.status(undefined).end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(500, /Invalid status code/, function (err) {
+            if (isIoJs) {
+              done(err ? null : new Error('expected error'))
+            } else {
+              done(err)
+            }
+          })
+      })
+    })
+
+    describe('when "code" is null', function () {
+      it('should raise error for invalid status code', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.status(null).end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(500, /Invalid status code/, function (err) {
+            if (isIoJs) {
+              done(err ? null : new Error('expected error'))
+            } else {
+              done(err)
+            }
+          })
+      })
+    })
+
+    describe('when "code" is 201', function () {
+      it('should set the response status code to 201', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.status(201).end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(201, done)
+      })
+    })
+
+    describe('when "code" is 302', function () {
+      it('should set the response status code to 302', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.status(302).end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(302, done)
+      })
+    })
+
+    describe('when "code" is 403', function () {
+      it('should set the response status code to 403', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.status(403).end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(403, done)
+      })
+    })
+
+    describe('when "code" is 501', function () {
+      it('should set the response status code to 501', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.status(501).end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(501, done)
+      })
+    })
+
+    describe('when "code" is "410"', function () {
+      it('should set the response status code to 410', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.status('410').end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(410, done)
+      })
+    })
+
+    describe('when "code" is 410.1', function () {
+      it('should set the response status code to 410', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.status(410.1).end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(410, function (err) {
+            if (isIoJs) {
+              done(err ? null : new Error('expected error'))
+            } else {
+              done(err)
+            }
+          })
+      })
+    })
+
+    describe('when "code" is 1000', function () {
+      it('should raise error for invalid status code', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.status(1000).end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(500, /Invalid status code/, function (err) {
+            if (isIoJs) {
+              done(err ? null : new Error('expected error'))
+            } else {
+              done(err)
+            }
+          })
+      })
+    })
+
+    describe('when "code" is 99', function () {
+      it('should raise error for invalid status code', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.status(99).end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(500, /Invalid status code/, function (err) {
+            if (isIoJs) {
+              done(err ? null : new Error('expected error'))
+            } else {
+              done(err)
+            }
+          })
+      })
+    })
+
+    describe('when "code" is -401', function () {
+      it('should raise error for invalid status code', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.status(-401).end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(500, /Invalid status code/, function (err) {
+            if (isIoJs) {
+              done(err ? null : new Error('expected error'))
+            } else {
+              done(err)
+            }
+          })
+      })
     })
   })
 })

From 0def9bb659557df1bd659411a1a6f2c5b8b9d893 Mon Sep 17 00:00:00 2001
From: Tommaso Tofacchi <tofacchitommaso@gmail.com>
Date: Fri, 11 Mar 2022 10:02:43 +0100
Subject: [PATCH 06/31] Add "root" option to res.download

fixes #4834
closes #4855
---
 History.md           |  1 +
 lib/response.js      |  4 ++-
 test/res.download.js | 74 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 78 insertions(+), 1 deletion(-)

diff --git a/History.md b/History.md
index 2e549136a5..d7a2e43c35 100644
--- a/History.md
+++ b/History.md
@@ -1,6 +1,7 @@
 unreleased
 ==========
 
+  * Add "root" option to `res.download`
   * Deprecate string and non-integer arguments to `res.status`
   * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
   * Support proper 205 responses using `res.send`
diff --git a/lib/response.js b/lib/response.js
index 7a9564d262..101311e0eb 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -582,7 +582,9 @@ res.download = function download (path, filename, options, callback) {
   opts.headers = headers
 
   // Resolve the full path for sendFile
-  var fullPath = resolve(path);
+  var fullPath = !opts.root
+    ? resolve(path)
+    : path
 
   // send file
   return this.sendFile(fullPath, opts, done)
diff --git a/test/res.download.js b/test/res.download.js
index 1322b0a31f..51380d4ba1 100644
--- a/test/res.download.js
+++ b/test/res.download.js
@@ -3,9 +3,12 @@
 var after = require('after');
 var Buffer = require('safe-buffer').Buffer
 var express = require('..');
+var path = require('path')
 var request = require('supertest');
 var utils = require('./support/utils')
 
+var FIXTURES_PATH = path.join(__dirname, 'fixtures')
+
 describe('res', function(){
   describe('.download(path)', function(){
     it('should transfer as an attachment', function(done){
@@ -178,6 +181,77 @@ describe('res', function(){
         .end(done)
       })
     })
+
+    describe('with "root" option', function () {
+      it('should allow relative path', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.download('name.txt', 'document', {
+            root: FIXTURES_PATH
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect(200)
+          .expect('Content-Disposition', 'attachment; filename="document"')
+          .expect(utils.shouldHaveBody(Buffer.from('tobi')))
+          .end(done)
+      })
+
+      it('should allow up within root', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.download('fake/../name.txt', 'document', {
+            root: FIXTURES_PATH
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect(200)
+          .expect('Content-Disposition', 'attachment; filename="document"')
+          .expect(utils.shouldHaveBody(Buffer.from('tobi')))
+          .end(done)
+      })
+
+      it('should reject up outside root', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          var p = '..' + path.sep +
+            path.relative(path.dirname(FIXTURES_PATH), path.join(FIXTURES_PATH, 'name.txt'))
+
+          res.download(p, 'document', {
+            root: FIXTURES_PATH
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect(403)
+          .expect(utils.shouldNotHaveHeader('Content-Disposition'))
+          .end(done)
+      })
+
+      it('should reject reading outside root', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.download('../name.txt', 'document', {
+            root: FIXTURES_PATH
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect(403)
+          .expect(utils.shouldNotHaveHeader('Content-Disposition'))
+          .end(done)
+      })
+    })
   })
 
   describe('on failure', function(){

From dd69eedd189eb55658ec435b821df13062cc5a8e Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Fri, 25 Mar 2022 01:43:45 -0400
Subject: [PATCH 07/31] deps: send@0.18.0

---
 History.md   | 8 ++++++++
 package.json | 2 +-
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/History.md b/History.md
index d7a2e43c35..c5242a99a4 100644
--- a/History.md
+++ b/History.md
@@ -5,6 +5,14 @@ unreleased
   * Deprecate string and non-integer arguments to `res.status`
   * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
   * Support proper 205 responses using `res.send`
+  * deps: send@0.18.0
+    - Fix emitted 416 error missing headers property
+    - Limit the headers removed for 304 response
+    - deps: depd@2.0.0
+    - deps: destroy@1.2.0
+    - deps: http-errors@2.0.0
+    - deps: on-finished@2.4.1
+    - deps: statuses@2.0.1
 
 4.17.3 / 2022-02-16
 ===================
diff --git a/package.json b/package.json
index 7992166629..1400007435 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
     "qs": "6.9.7",
     "range-parser": "~1.2.1",
     "safe-buffer": "5.2.1",
-    "send": "0.17.2",
+    "send": "0.18.0",
     "serve-static": "1.14.2",
     "setprototypeof": "1.2.0",
     "statuses": "~1.5.0",

From c92420648e18f78d22db24e0ffec99155ec54a49 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Fri, 25 Mar 2022 01:44:51 -0400
Subject: [PATCH 08/31] deps: serve-static@1.15.0

---
 History.md   | 2 ++
 package.json | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/History.md b/History.md
index c5242a99a4..9e4f0d3a8e 100644
--- a/History.md
+++ b/History.md
@@ -13,6 +13,8 @@ unreleased
     - deps: http-errors@2.0.0
     - deps: on-finished@2.4.1
     - deps: statuses@2.0.1
+  * deps: serve-static@1.15.0
+    - deps: send@0.18.0
 
 4.17.3 / 2022-02-16
 ===================
diff --git a/package.json b/package.json
index 1400007435..c733bb2e50 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,7 @@
     "range-parser": "~1.2.1",
     "safe-buffer": "5.2.1",
     "send": "0.18.0",
-    "serve-static": "1.14.2",
+    "serve-static": "1.15.0",
     "setprototypeof": "1.2.0",
     "statuses": "~1.5.0",
     "type-is": "~1.6.18",

From f739b162d9cc9fcfc1f514c2441c69f9fcb4364d Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Fri, 25 Mar 2022 01:46:32 -0400
Subject: [PATCH 09/31] deps: finalhandler@1.2.0

---
 History.md   | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/History.md b/History.md
index 9e4f0d3a8e..03b7ba9c7d 100644
--- a/History.md
+++ b/History.md
@@ -5,6 +5,10 @@ unreleased
   * Deprecate string and non-integer arguments to `res.status`
   * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
   * Support proper 205 responses using `res.send`
+  * deps: finalhandler@1.2.0
+    - Remove set content headers that break response
+    - deps: on-finished@2.4.1
+    - deps: statuses@2.0.1
   * deps: send@0.18.0
     - Fix emitted 416 error missing headers property
     - Limit the headers removed for 304 response
diff --git a/package.json b/package.json
index c733bb2e50..6de4bfff42 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
     "encodeurl": "~1.0.2",
     "escape-html": "~1.0.3",
     "etag": "~1.8.1",
-    "finalhandler": "~1.1.2",
+    "finalhandler": "1.2.0",
     "fresh": "0.5.2",
     "merge-descriptors": "1.0.1",
     "methods": "~1.1.2",

From 03dc3671874b214f67dacaca90f39a1c389f822e Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Fri, 25 Mar 2022 18:13:42 -0400
Subject: [PATCH 10/31] Allow options without filename in res.download

---
 History.md           |   1 +
 lib/response.js      |   7 ++
 test/res.download.js | 260 ++++++++++++++++++++++++++++++++-----------
 3 files changed, 206 insertions(+), 62 deletions(-)

diff --git a/History.md b/History.md
index 03b7ba9c7d..9b4bf4bce5 100644
--- a/History.md
+++ b/History.md
@@ -2,6 +2,7 @@ unreleased
 ==========
 
   * Add "root" option to `res.download`
+  * Allow `options` without `filename` in `res.download`
   * Deprecate string and non-integer arguments to `res.status`
   * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
   * Support proper 205 responses using `res.send`
diff --git a/lib/response.js b/lib/response.js
index 101311e0eb..3713e6f9a9 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -561,6 +561,13 @@ res.download = function download (path, filename, options, callback) {
     opts = null
   }
 
+  // support optional filename, where options may be in it's place
+  if (typeof filename === 'object' &&
+    (typeof options === 'function' || options === undefined)) {
+    name = null
+    opts = filename
+  }
+
   // set Content-Disposition when file is sent
   var headers = {
     'Content-Disposition': contentDisposition(name || path)
diff --git a/test/res.download.js b/test/res.download.js
index 51380d4ba1..91b074e8bf 100644
--- a/test/res.download.js
+++ b/test/res.download.js
@@ -86,46 +86,12 @@ describe('res', function(){
     })
   })
 
-  describe('.download(path, filename, fn)', function(){
-    it('should invoke the callback', function(done){
-      var app = express();
-      var cb = after(2, done);
-
-      app.use(function(req, res){
-        res.download('test/fixtures/user.html', 'document', cb)
-      });
-
-      request(app)
-      .get('/')
-      .expect('Content-Type', 'text/html; charset=UTF-8')
-      .expect('Content-Disposition', 'attachment; filename="document"')
-      .expect(200, cb);
-    })
-  })
-
-  describe('.download(path, filename, options, fn)', function () {
-    it('should invoke the callback', function (done) {
-      var app = express()
-      var cb = after(2, done)
-      var options = {}
-
-      app.use(function (req, res) {
-        res.download('test/fixtures/user.html', 'document', options, cb)
-      })
-
-      request(app)
-      .get('/')
-      .expect(200)
-      .expect('Content-Type', 'text/html; charset=UTF-8')
-      .expect('Content-Disposition', 'attachment; filename="document"')
-      .end(cb)
-    })
-
+  describe('.download(path, options)', function () {
     it('should allow options to res.sendFile()', function (done) {
       var app = express()
 
       app.use(function (req, res) {
-        res.download('test/fixtures/.name', 'document', {
+        res.download('test/fixtures/.name', {
           dotfiles: 'allow',
           maxAge: '4h'
         })
@@ -134,51 +100,124 @@ describe('res', function(){
       request(app)
         .get('/')
         .expect(200)
-        .expect('Content-Disposition', 'attachment; filename="document"')
+        .expect('Content-Disposition', 'attachment; filename=".name"')
         .expect('Cache-Control', 'public, max-age=14400')
         .expect(utils.shouldHaveBody(Buffer.from('tobi')))
         .end(done)
     })
 
-    describe('when options.headers contains Content-Disposition', function () {
-      it('should be ignored', function (done) {
+    describe('with "headers" option', function () {
+      it('should set headers on response', function (done) {
         var app = express()
 
         app.use(function (req, res) {
-          res.download('test/fixtures/user.html', 'document', {
+          res.download('test/fixtures/user.html', {
             headers: {
-              'Content-Type': 'text/x-custom',
-              'Content-Disposition': 'inline'
+              'X-Foo': 'Bar',
+              'X-Bar': 'Foo'
             }
           })
         })
 
         request(app)
-        .get('/')
-        .expect(200)
-        .expect('Content-Type', 'text/x-custom')
-        .expect('Content-Disposition', 'attachment; filename="document"')
-        .end(done)
+          .get('/')
+          .expect(200)
+          .expect('X-Foo', 'Bar')
+          .expect('X-Bar', 'Foo')
+          .end(done)
       })
 
-      it('should be ignored case-insensitively', function (done) {
+      it('should use last header when duplicated', function (done) {
         var app = express()
 
         app.use(function (req, res) {
-          res.download('test/fixtures/user.html', 'document', {
+          res.download('test/fixtures/user.html', {
             headers: {
-              'content-type': 'text/x-custom',
-              'content-disposition': 'inline'
+              'X-Foo': 'Bar',
+              'x-foo': 'bar'
             }
           })
         })
 
         request(app)
-        .get('/')
-        .expect(200)
-        .expect('Content-Type', 'text/x-custom')
-        .expect('Content-Disposition', 'attachment; filename="document"')
-        .end(done)
+          .get('/')
+          .expect(200)
+          .expect('X-Foo', 'bar')
+          .end(done)
+      })
+
+      it('should override Content-Type', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.download('test/fixtures/user.html', {
+            headers: {
+              'Content-Type': 'text/x-custom'
+            }
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect(200)
+          .expect('Content-Type', 'text/x-custom')
+          .end(done)
+      })
+
+      it('should not set headers on 404', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.download('test/fixtures/does-not-exist', {
+            headers: {
+              'X-Foo': 'Bar'
+            }
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect(404)
+          .expect(utils.shouldNotHaveHeader('X-Foo'))
+          .end(done)
+      })
+
+      describe('when headers contains Content-Disposition', function () {
+        it('should be ignored', function (done) {
+          var app = express()
+
+          app.use(function (req, res) {
+            res.download('test/fixtures/user.html', {
+              headers: {
+                'Content-Disposition': 'inline'
+              }
+            })
+          })
+
+          request(app)
+            .get('/')
+            .expect(200)
+            .expect('Content-Disposition', 'attachment; filename="user.html"')
+            .end(done)
+        })
+
+        it('should be ignored case-insensitively', function (done) {
+          var app = express()
+
+          app.use(function (req, res) {
+            res.download('test/fixtures/user.html', {
+              headers: {
+                'content-disposition': 'inline'
+              }
+            })
+          })
+
+          request(app)
+            .get('/')
+            .expect(200)
+            .expect('Content-Disposition', 'attachment; filename="user.html"')
+            .end(done)
+        })
       })
     })
 
@@ -187,7 +226,7 @@ describe('res', function(){
         var app = express()
 
         app.use(function (req, res) {
-          res.download('name.txt', 'document', {
+          res.download('name.txt', {
             root: FIXTURES_PATH
           })
         })
@@ -195,7 +234,7 @@ describe('res', function(){
         request(app)
           .get('/')
           .expect(200)
-          .expect('Content-Disposition', 'attachment; filename="document"')
+          .expect('Content-Disposition', 'attachment; filename="name.txt"')
           .expect(utils.shouldHaveBody(Buffer.from('tobi')))
           .end(done)
       })
@@ -204,7 +243,7 @@ describe('res', function(){
         var app = express()
 
         app.use(function (req, res) {
-          res.download('fake/../name.txt', 'document', {
+          res.download('fake/../name.txt', {
             root: FIXTURES_PATH
           })
         })
@@ -212,7 +251,7 @@ describe('res', function(){
         request(app)
           .get('/')
           .expect(200)
-          .expect('Content-Disposition', 'attachment; filename="document"')
+          .expect('Content-Disposition', 'attachment; filename="name.txt"')
           .expect(utils.shouldHaveBody(Buffer.from('tobi')))
           .end(done)
       })
@@ -224,7 +263,7 @@ describe('res', function(){
           var p = '..' + path.sep +
             path.relative(path.dirname(FIXTURES_PATH), path.join(FIXTURES_PATH, 'name.txt'))
 
-          res.download(p, 'document', {
+          res.download(p, {
             root: FIXTURES_PATH
           })
         })
@@ -240,7 +279,7 @@ describe('res', function(){
         var app = express()
 
         app.use(function (req, res) {
-          res.download('../name.txt', 'document', {
+          res.download('../name.txt', {
             root: FIXTURES_PATH
           })
         })
@@ -254,6 +293,103 @@ describe('res', function(){
     })
   })
 
+  describe('.download(path, filename, fn)', function(){
+    it('should invoke the callback', function(done){
+      var app = express();
+      var cb = after(2, done);
+
+      app.use(function(req, res){
+        res.download('test/fixtures/user.html', 'document', cb)
+      });
+
+      request(app)
+      .get('/')
+      .expect('Content-Type', 'text/html; charset=UTF-8')
+      .expect('Content-Disposition', 'attachment; filename="document"')
+      .expect(200, cb);
+    })
+  })
+
+  describe('.download(path, filename, options, fn)', function () {
+    it('should invoke the callback', function (done) {
+      var app = express()
+      var cb = after(2, done)
+      var options = {}
+
+      app.use(function (req, res) {
+        res.download('test/fixtures/user.html', 'document', options, cb)
+      })
+
+      request(app)
+      .get('/')
+      .expect(200)
+      .expect('Content-Type', 'text/html; charset=UTF-8')
+      .expect('Content-Disposition', 'attachment; filename="document"')
+      .end(cb)
+    })
+
+    it('should allow options to res.sendFile()', function (done) {
+      var app = express()
+
+      app.use(function (req, res) {
+        res.download('test/fixtures/.name', 'document', {
+          dotfiles: 'allow',
+          maxAge: '4h'
+        })
+      })
+
+      request(app)
+        .get('/')
+        .expect(200)
+        .expect('Content-Disposition', 'attachment; filename="document"')
+        .expect('Cache-Control', 'public, max-age=14400')
+        .expect(utils.shouldHaveBody(Buffer.from('tobi')))
+        .end(done)
+    })
+
+    describe('when options.headers contains Content-Disposition', function () {
+      it('should be ignored', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.download('test/fixtures/user.html', 'document', {
+            headers: {
+              'Content-Type': 'text/x-custom',
+              'Content-Disposition': 'inline'
+            }
+          })
+        })
+
+        request(app)
+        .get('/')
+        .expect(200)
+        .expect('Content-Type', 'text/x-custom')
+        .expect('Content-Disposition', 'attachment; filename="document"')
+        .end(done)
+      })
+
+      it('should be ignored case-insensitively', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.download('test/fixtures/user.html', 'document', {
+            headers: {
+              'content-type': 'text/x-custom',
+              'content-disposition': 'inline'
+            }
+          })
+        })
+
+        request(app)
+        .get('/')
+        .expect(200)
+        .expect('Content-Type', 'text/x-custom')
+        .expect('Content-Disposition', 'attachment; filename="document"')
+        .end(done)
+      })
+    })
+  })
+
   describe('on failure', function(){
     it('should invoke the callback', function(done){
       var app = express();

From 10b9b507b7d113d04965cccd8d170ee524e3d555 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Fri, 25 Mar 2022 18:14:27 -0400
Subject: [PATCH 11/31] examples: use updated res.download in example

---
 examples/downloads/index.js | 5 +----
 package.json                | 1 -
 2 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/examples/downloads/index.js b/examples/downloads/index.js
index 62e7fa6e3e..6b67e0c886 100644
--- a/examples/downloads/index.js
+++ b/examples/downloads/index.js
@@ -6,7 +6,6 @@
 
 var express = require('../../');
 var path = require('path');
-var resolvePath = require('resolve-path')
 
 var app = module.exports = express();
 
@@ -25,9 +24,7 @@ app.get('/', function(req, res){
 // /files/* is accessed via req.params[0]
 // but here we name it :file
 app.get('/files/:file(*)', function(req, res, next){
-  var filePath = resolvePath(FILES_DIR, req.params.file)
-
-  res.download(filePath, function (err) {
+  res.download(req.params.file, { root: FILES_DIR }, function (err) {
     if (!err) return; // file sent
     if (err.status !== 404) return next(err); // non-404 error
     // file for download not found
diff --git a/package.json b/package.json
index 6de4bfff42..8f8959a4c3 100644
--- a/package.json
+++ b/package.json
@@ -75,7 +75,6 @@
     "multiparty": "4.2.3",
     "nyc": "15.1.0",
     "pbkdf2-password": "1.2.1",
-    "resolve-path": "1.4.0",
     "supertest": "6.2.2",
     "vhost": "~3.0.2"
   },

From 9482b82d0b9c498140561087d68f0409078a86ea Mon Sep 17 00:00:00 2001
From: Nadav Ivgi <nadav@shesek.info>
Date: Tue, 13 Mar 2018 08:14:06 +0200
Subject: [PATCH 12/31] Invoke default with same arguments as types in
 res.format

closes #3587
---
 History.md         |  1 +
 lib/response.js    |  9 ++++-----
 test/res.format.js | 29 ++++++++++++++++++++++++++++-
 3 files changed, 33 insertions(+), 6 deletions(-)

diff --git a/History.md b/History.md
index 9b4bf4bce5..d1aa9b3b88 100644
--- a/History.md
+++ b/History.md
@@ -5,6 +5,7 @@ unreleased
   * Allow `options` without `filename` in `res.download`
   * Deprecate string and non-integer arguments to `res.status`
   * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
+  * Invoke `default` with same arguments as types in `res.format`
   * Support proper 205 responses using `res.send`
   * deps: finalhandler@1.2.0
     - Remove set content headers that break response
diff --git a/lib/response.js b/lib/response.js
index 3713e6f9a9..bfa7871434 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -684,9 +684,8 @@ res.format = function(obj){
   var req = this.req;
   var next = req.next;
 
-  var fn = obj.default;
-  if (fn) delete obj.default;
-  var keys = Object.keys(obj);
+  var keys = Object.keys(obj)
+    .filter(function (v) { return v !== 'default' })
 
   var key = keys.length > 0
     ? req.accepts(keys)
@@ -697,8 +696,8 @@ res.format = function(obj){
   if (key) {
     this.set('Content-Type', normalizeType(key).value);
     obj[key](req, this, next);
-  } else if (fn) {
-    fn();
+  } else if (obj.default) {
+    obj.default(req, this, next)
   } else {
     var err = new Error('Not Acceptable');
     err.status = err.statusCode = 406;
diff --git a/test/res.format.js b/test/res.format.js
index 24e18d9552..45243d17a1 100644
--- a/test/res.format.js
+++ b/test/res.format.js
@@ -50,7 +50,12 @@ var app3 = express();
 app3.use(function(req, res, next){
   res.format({
     text: function(){ res.send('hey') },
-    default: function(){ res.send('default') }
+    default: function (a, b, c) {
+      assert(req === a)
+      assert(res === b)
+      assert(next === c)
+      res.send('default')
+    }
   })
 });
 
@@ -118,6 +123,28 @@ describe('res', function(){
         .set('Accept', '*/*')
         .expect('hey', done);
       })
+
+      it('should be able to invoke other formatter', function (done) {
+        var app = express()
+
+        app.use(function (req, res, next) {
+          res.format({
+            json: function () { res.send('json') },
+            default: function () {
+              res.header('x-default', '1')
+              this.json()
+            }
+          })
+        })
+
+        request(app)
+          .get('/')
+          .set('Accept', 'text/plain')
+          .expect(200)
+          .expect('x-default', '1')
+          .expect('json')
+          .end(done)
+      })
     })
 
     describe('in router', function(){

From 1cc816993832eba829a2f556f7c08e27e6371301 Mon Sep 17 00:00:00 2001
From: Ulises Gascon <ulises.gascon@guidesmiths.com>
Date: Wed, 5 Feb 2020 17:56:51 +0100
Subject: [PATCH 13/31] deps: depd@2.0.0

closes #4174
---
 History.md   | 3 +++
 package.json | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/History.md b/History.md
index d1aa9b3b88..0b54e1d27b 100644
--- a/History.md
+++ b/History.md
@@ -7,6 +7,9 @@ unreleased
   * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
   * Invoke `default` with same arguments as types in `res.format`
   * Support proper 205 responses using `res.send`
+  * deps: depd@2.0.0
+    - Replace internal `eval` usage with `Function` constructor
+    - Use instance methods on `process` to check for listeners
   * deps: finalhandler@1.2.0
     - Remove set content headers that break response
     - deps: on-finished@2.4.1
diff --git a/package.json b/package.json
index 8f8959a4c3..d9fbe97698 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
     "cookie": "0.4.2",
     "cookie-signature": "1.0.6",
     "debug": "2.6.9",
-    "depd": "~1.1.2",
+    "depd": "2.0.0",
     "encodeurl": "~1.0.2",
     "escape-html": "~1.0.3",
     "etag": "~1.8.1",

From 5855339455a7f60774bef4166829e742a5056fa8 Mon Sep 17 00:00:00 2001
From: Chris Barth <chrisjbarth@hotmail.com>
Date: Thu, 18 Apr 2019 09:12:33 -0400
Subject: [PATCH 14/31] Fix behavior of null/undefined as "maxAge" in
 res.cookie

fixes #3935
closes #3936
---
 History.md         |  1 +
 lib/response.js    | 10 +++++++---
 test/res.cookie.js | 30 ++++++++++++++++++++++++++++++
 3 files changed, 38 insertions(+), 3 deletions(-)

diff --git a/History.md b/History.md
index 0b54e1d27b..8d9d39b2b8 100644
--- a/History.md
+++ b/History.md
@@ -4,6 +4,7 @@ unreleased
   * Add "root" option to `res.download`
   * Allow `options` without `filename` in `res.download`
   * Deprecate string and non-integer arguments to `res.status`
+  * Fix behavior of `null`/`undefined` as `maxAge` in `res.cookie`
   * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
   * Invoke `default` with same arguments as types in `res.format`
   * Support proper 205 responses using `res.send`
diff --git a/lib/response.js b/lib/response.js
index bfa7871434..eeeee1c806 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -868,9 +868,13 @@ res.cookie = function (name, value, options) {
     val = 's:' + sign(val, secret);
   }
 
-  if ('maxAge' in opts) {
-    opts.expires = new Date(Date.now() + opts.maxAge);
-    opts.maxAge /= 1000;
+  if (opts.maxAge != null) {
+    var maxAge = opts.maxAge - 0
+
+    if (!isNaN(maxAge)) {
+      opts.expires = new Date(Date.now() + maxAge)
+      opts.maxAge = Math.floor(maxAge / 1000)
+    }
   }
 
   if (opts.path == null) {
diff --git a/test/res.cookie.js b/test/res.cookie.js
index d10e48646b..e3a921301f 100644
--- a/test/res.cookie.js
+++ b/test/res.cookie.js
@@ -111,6 +111,36 @@ describe('res', function(){
         .expect(200, optionsCopy, done)
       })
 
+      it('should not throw on null', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.cookie('name', 'tobi', { maxAge: null })
+          res.end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(200)
+          .expect('Set-Cookie', 'name=tobi; Path=/')
+          .end(done)
+      })
+
+      it('should not throw on undefined', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.cookie('name', 'tobi', { maxAge: undefined })
+          res.end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(200)
+          .expect('Set-Cookie', 'name=tobi; Path=/')
+          .end(done)
+      })
+
       it('should throw an error with invalid maxAge', function (done) {
         var app = express()
 

From a10770286e7420c5a56ed3cc0b6add2c028ae56e Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Sun, 27 Mar 2022 23:41:31 -0400
Subject: [PATCH 15/31] Use http-errors for res.format error

---
 History.md      | 1 +
 lib/response.js | 8 ++++----
 package.json    | 1 +
 3 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/History.md b/History.md
index 8d9d39b2b8..2e542333d9 100644
--- a/History.md
+++ b/History.md
@@ -8,6 +8,7 @@ unreleased
   * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
   * 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: depd@2.0.0
     - Replace internal `eval` usage with `Function` constructor
     - Use instance methods on `process` to check for listeners
diff --git a/lib/response.js b/lib/response.js
index eeeee1c806..d9b8db1c20 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -14,6 +14,7 @@
 
 var Buffer = require('safe-buffer').Buffer
 var contentDisposition = require('content-disposition');
+var createError = require('http-errors')
 var deprecate = require('depd')('express');
 var encodeUrl = require('encodeurl');
 var escapeHtml = require('escape-html');
@@ -699,10 +700,9 @@ res.format = function(obj){
   } else if (obj.default) {
     obj.default(req, this, next)
   } else {
-    var err = new Error('Not Acceptable');
-    err.status = err.statusCode = 406;
-    err.types = normalizeTypes(keys).map(function(o){ return o.value });
-    next(err);
+    next(createError(406, {
+      types: normalizeTypes(keys).map(function (o) { return o.value })
+    }))
   }
 
   return this;
diff --git a/package.json b/package.json
index d9fbe97698..71c110e5f2 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
     "etag": "~1.8.1",
     "finalhandler": "1.2.0",
     "fresh": "0.5.2",
+    "http-errors": "2.0.0",
     "merge-descriptors": "1.0.1",
     "methods": "~1.1.2",
     "on-finished": "~2.3.0",

From 32c558d414b4ac0f5bd70fa6a0f39a5558a7b016 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Sat, 2 Apr 2022 21:51:31 -0400
Subject: [PATCH 16/31] deps: body-parser@1.20.0

---
 History.md                 |  10 ++
 package.json               |   2 +-
 test/express.json.js       | 339 +++++++++++++++++++++++++------------
 test/express.raw.js        | 203 ++++++++++++++++++++--
 test/express.text.js       | 221 ++++++++++++++++++++----
 test/express.urlencoded.js | 263 ++++++++++++++++++++--------
 6 files changed, 817 insertions(+), 221 deletions(-)

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 {}
+  }
+}

From 1df75763e315bd0582669238cd14baadec1d6db5 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Sat, 2 Apr 2022 21:56:41 -0400
Subject: [PATCH 17/31] deps: qs@6.10.3

---
 History.md   | 1 +
 package.json | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/History.md b/History.md
index cba82afcbd..ef5ce5fc02 100644
--- a/History.md
+++ b/History.md
@@ -26,6 +26,7 @@ unreleased
     - Remove set content headers that break response
     - deps: on-finished@2.4.1
     - deps: statuses@2.0.1
+  * deps: qs@6.10.3
   * deps: send@0.18.0
     - Fix emitted 416 error missing headers property
     - Limit the headers removed for 304 response
diff --git a/package.json b/package.json
index ce6604bd7d..04e39b06e1 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
     "parseurl": "~1.3.3",
     "path-to-regexp": "0.1.7",
     "proxy-addr": "~2.0.7",
-    "qs": "6.9.7",
+    "qs": "6.10.3",
     "range-parser": "~1.2.1",
     "safe-buffer": "5.2.1",
     "send": "0.18.0",

From 980d881e3b023db079de60477a2588a91f046ca5 Mon Sep 17 00:00:00 2001
From: 3imed-jaberi <imed_jebari@hotmail.fr>
Date: Fri, 3 Jul 2020 05:38:59 +0200
Subject: [PATCH 18/31] deps: statuses@2.0.1

closes #4336
---
 History.md      | 3 +++
 lib/response.js | 8 ++++----
 package.json    | 2 +-
 3 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/History.md b/History.md
index ef5ce5fc02..ef9bbb6e42 100644
--- a/History.md
+++ b/History.md
@@ -37,6 +37,9 @@ unreleased
     - deps: statuses@2.0.1
   * deps: serve-static@1.15.0
     - deps: send@0.18.0
+  * deps: statuses@2.0.1
+    - Remove code 306
+    - Rename `425 Unordered Collection` to standard `425 Too Early`
 
 4.17.3 / 2022-02-16
 ===================
diff --git a/lib/response.js b/lib/response.js
index d9b8db1c20..fede486c06 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -139,7 +139,7 @@ res.send = function send(body) {
 
     deprecate('res.send(status): Use res.sendStatus(status) instead');
     this.statusCode = chunk;
-    chunk = statuses[chunk]
+    chunk = statuses.message[chunk]
   }
 
   switch (typeof chunk) {
@@ -367,7 +367,7 @@ res.jsonp = function jsonp(obj) {
  */
 
 res.sendStatus = function sendStatus(statusCode) {
-  var body = statuses[statusCode] || String(statusCode)
+  var body = statuses.message[statusCode] || String(statusCode)
 
   this.statusCode = statusCode;
   this.type('txt');
@@ -955,12 +955,12 @@ res.redirect = function redirect(url) {
   // Support text/{plain,html} by default
   this.format({
     text: function(){
-      body = statuses[status] + '. Redirecting to ' + address
+      body = statuses.message[status] + '. Redirecting to ' + address
     },
 
     html: function(){
       var u = escapeHtml(address);
-      body = '<p>' + statuses[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>'
+      body = '<p>' + statuses.message[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>'
     },
 
     default: function(){
diff --git a/package.json b/package.json
index 04e39b06e1..1e1f674097 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
     "send": "0.18.0",
     "serve-static": "1.15.0",
     "setprototypeof": "1.2.0",
-    "statuses": "~1.5.0",
+    "statuses": "2.0.1",
     "type-is": "~1.6.18",
     "utils-merge": "1.0.1",
     "vary": "~1.1.2"

From 2e2d78c4d99829250018c6e4d20f3c6377a90683 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Sun, 3 Apr 2022 01:15:37 -0400
Subject: [PATCH 19/31] deps: on-finished@2.4.1

---
 History.md           |   2 +
 package.json         |   2 +-
 test/res.download.js |  73 ++++++++++++++++++++++++
 test/res.sendFile.js | 129 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 205 insertions(+), 1 deletion(-)

diff --git a/History.md b/History.md
index ef9bbb6e42..84efe9b4d8 100644
--- a/History.md
+++ b/History.md
@@ -26,6 +26,8 @@ unreleased
     - Remove set content headers that break response
     - deps: on-finished@2.4.1
     - deps: statuses@2.0.1
+  * deps: on-finished@2.4.1
+    - Prevent loss of async hooks context
   * deps: qs@6.10.3
   * deps: send@0.18.0
     - Fix emitted 416 error missing headers property
diff --git a/package.json b/package.json
index 1e1f674097..dfce12352c 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
     "http-errors": "2.0.0",
     "merge-descriptors": "1.0.1",
     "methods": "~1.1.2",
-    "on-finished": "~2.3.0",
+    "on-finished": "2.4.1",
     "parseurl": "~1.3.3",
     "path-to-regexp": "0.1.7",
     "proxy-addr": "~2.0.7",
diff --git a/test/res.download.js b/test/res.download.js
index 91b074e8bf..b52e66803c 100644
--- a/test/res.download.js
+++ b/test/res.download.js
@@ -1,6 +1,8 @@
 'use strict'
 
 var after = require('after');
+var assert = require('assert')
+var asyncHooks = tryRequire('async_hooks')
 var Buffer = require('safe-buffer').Buffer
 var express = require('..');
 var path = require('path')
@@ -9,6 +11,10 @@ var utils = require('./support/utils')
 
 var FIXTURES_PATH = path.join(__dirname, 'fixtures')
 
+var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
+  ? describe
+  : describe.skip
+
 describe('res', function(){
   describe('.download(path)', function(){
     it('should transfer as an attachment', function(done){
@@ -84,6 +90,65 @@ describe('res', function(){
       .expect('Content-Disposition', 'attachment; filename="user.html"')
       .expect(200, cb);
     })
+
+    describeAsyncHooks('async local storage', function () {
+      it('should presist store', function (done) {
+        var app = express()
+        var cb = after(2, done)
+        var store = { foo: 'bar' }
+
+        app.use(function (req, res, next) {
+          req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage()
+          req.asyncLocalStorage.run(store, next)
+        })
+
+        app.use(function (req, res) {
+          res.download('test/fixtures/name.txt', function (err) {
+            if (err) return cb(err)
+
+            var local = req.asyncLocalStorage.getStore()
+
+            assert.strictEqual(local.foo, 'bar')
+            cb()
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect('Content-Type', 'text/plain; charset=UTF-8')
+          .expect('Content-Disposition', 'attachment; filename="name.txt"')
+          .expect(200, 'tobi', cb)
+      })
+
+      it('should presist store on error', function (done) {
+        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(function (req, res) {
+          res.download('test/fixtures/does-not-exist', function (err) {
+            var local = req.asyncLocalStorage.getStore()
+
+            if (local) {
+              res.setHeader('x-store-foo', String(local.foo))
+            }
+
+            res.send(err ? 'got ' + err.status + ' error' : 'no error')
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect(200)
+          .expect('x-store-foo', 'bar')
+          .expect('got 404 error')
+          .end(done)
+      })
+    })
   })
 
   describe('.download(path, options)', function () {
@@ -423,3 +488,11 @@ describe('res', function(){
     })
   })
 })
+
+function tryRequire (name) {
+  try {
+    return require(name)
+  } catch (e) {
+    return {}
+  }
+}
diff --git a/test/res.sendFile.js b/test/res.sendFile.js
index e828c17e25..ba5c33516b 100644
--- a/test/res.sendFile.js
+++ b/test/res.sendFile.js
@@ -1,6 +1,7 @@
 'use strict'
 
 var after = require('after');
+var asyncHooks = tryRequire('async_hooks')
 var Buffer = require('safe-buffer').Buffer
 var express = require('../')
   , request = require('supertest')
@@ -10,6 +11,10 @@ var path = require('path');
 var fixtures = path.join(__dirname, 'fixtures');
 var utils = require('./support/utils');
 
+var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
+  ? describe
+  : describe.skip
+
 describe('res', function(){
   describe('.sendFile(path)', function () {
     it('should error missing path', function (done) {
@@ -261,6 +266,64 @@ describe('res', function(){
         .get('/')
         .expect(200, 'got 404 error', done)
     })
+
+    describeAsyncHooks('async local storage', function () {
+      it('should presist store', function (done) {
+        var app = express()
+        var cb = after(2, done)
+        var store = { foo: 'bar' }
+
+        app.use(function (req, res, next) {
+          req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage()
+          req.asyncLocalStorage.run(store, next)
+        })
+
+        app.use(function (req, res) {
+          res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) {
+            if (err) return cb(err)
+
+            var local = req.asyncLocalStorage.getStore()
+
+            assert.strictEqual(local.foo, 'bar')
+            cb()
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect('Content-Type', 'text/plain; charset=UTF-8')
+          .expect(200, 'tobi', cb)
+      })
+
+      it('should presist store on error', function (done) {
+        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(function (req, res) {
+          res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) {
+            var local = req.asyncLocalStorage.getStore()
+
+            if (local) {
+              res.setHeader('x-store-foo', String(local.foo))
+            }
+
+            res.send(err ? 'got ' + err.status + ' error' : 'no error')
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect(200)
+          .expect('x-store-foo', 'bar')
+          .expect('got 404 error')
+          .end(done)
+      })
+    })
   })
 
   describe('.sendFile(path, options)', function () {
@@ -999,6 +1062,64 @@ describe('res', function(){
       .get('/')
       .end(function(){});
     })
+
+    describeAsyncHooks('async local storage', function () {
+      it('should presist store', function (done) {
+        var app = express()
+        var cb = after(2, done)
+        var store = { foo: 'bar' }
+
+        app.use(function (req, res, next) {
+          req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage()
+          req.asyncLocalStorage.run(store, next)
+        })
+
+        app.use(function (req, res) {
+          res.sendfile('test/fixtures/name.txt', function (err) {
+            if (err) return cb(err)
+
+            var local = req.asyncLocalStorage.getStore()
+
+            assert.strictEqual(local.foo, 'bar')
+            cb()
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect('Content-Type', 'text/plain; charset=UTF-8')
+          .expect(200, 'tobi', cb)
+      })
+
+      it('should presist store on error', function (done) {
+        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(function (req, res) {
+          res.sendfile('test/fixtures/does-not-exist', function (err) {
+            var local = req.asyncLocalStorage.getStore()
+
+            if (local) {
+              res.setHeader('x-store-foo', String(local.foo))
+            }
+
+            res.send(err ? 'got ' + err.status + ' error' : 'no error')
+          })
+        })
+
+        request(app)
+          .get('/')
+          .expect(200)
+          .expect('x-store-foo', 'bar')
+          .expect('got 404 error')
+          .end(done)
+      })
+    })
   })
 
   describe('.sendfile(path)', function(){
@@ -1280,3 +1401,11 @@ function createApp(path, options, fn) {
 
   return app;
 }
+
+function tryRequire (name) {
+  try {
+    return require(name)
+  } catch (e) {
+    return {}
+  }
+}

From 04da4aaf1a484e81856fc4713340300e4d84d573 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Thu, 7 Apr 2022 19:17:10 -0400
Subject: [PATCH 20/31] build: use supertest@3.4.2 for Node.js 6.x

---
 .github/workflows/ci.yml | 2 +-
 appveyor.yml             | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7b153d1b43..83a8edee87 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -61,7 +61,7 @@ jobs:
 
         - name: Node.js 6.x
           node-version: "6.17"
-          npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6
+          npm-i: mocha@6.2.2 nyc@14.1.1 supertest@3.4.2
 
         - name: Node.js 7.x
           node-version: "7.10"
diff --git a/appveyor.yml b/appveyor.yml
index 2a2507b411..93ea2c8161 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -71,11 +71,11 @@ install:
   - ps: |
       # supertest for http calls
       # - use 2.0.0 for Node.js < 4
-      # - use 3.4.2 for Node.js < 6
+      # - use 3.4.2 for Node.js < 7
       # - use 6.1.6 for Node.js < 8
       if ([int]$env:nodejs_version.split(".")[0] -lt 4) {
         npm install --silent --save-dev supertest@2.0.0
-      } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) {
+      } elseif ([int]$env:nodejs_version.split(".")[0] -lt 7) {
         npm install --silent --save-dev supertest@3.4.2
       } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) {
         npm install --silent --save-dev supertest@6.1.6

From 1b2e097be2f5b62b7db7dae09f399ace54836e0a Mon Sep 17 00:00:00 2001
From: Hashen <37979557+Hashen110@users.noreply.github.com>
Date: Thu, 7 Apr 2022 21:14:16 +0530
Subject: [PATCH 21/31] tests: fix typo in description

closes #4882
---
 test/res.sendFile.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/res.sendFile.js b/test/res.sendFile.js
index ba5c33516b..eb71adeb6a 100644
--- a/test/res.sendFile.js
+++ b/test/res.sendFile.js
@@ -747,7 +747,7 @@ describe('res', function(){
       })
 
       describe('when cacheControl: false', function () {
-        it('shold not send cache-control', function (done) {
+        it('should not send cache-control', function (done) {
           var app = express()
 
           app.use(function (req, res) {

From 99175c3ef63166d199bab8f402103522dec5f0ee Mon Sep 17 00:00:00 2001
From: Ghouse Mohamed <ghousemohamed7126@gmail.com>
Date: Sun, 27 Mar 2022 05:15:01 +0530
Subject: [PATCH 22/31] docs: fix typo in casing of HTTP

closes #4872
---
 Charter.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Charter.md b/Charter.md
index f9647cb734..a906e52909 100644
--- a/Charter.md
+++ b/Charter.md
@@ -9,7 +9,7 @@ also easily visible to outsiders.
 
 ## Section 1: Scope
 
-Express is a http web server framework with a simple and expressive API
+Express is a HTTP web server framework with a simple and expressive API
 which is highly aligned with Node.js core. We aim to be the best in
 class for writing performant, spec compliant, and powerful web servers
 in Node.js. As one of the oldest and most popular web frameworks in
@@ -24,7 +24,7 @@ Express is made of many modules spread between three GitHub Orgs:
   libraries
 - [pillarjs](http://github.com/pillarjs/): Components which make up
   Express but can also be used for other web frameworks
-- [jshttp](http://github.com/jshttp/): Low level http libraries
+- [jshttp](http://github.com/jshttp/): Low level HTTP libraries
 
 ### 1.2: Out-of-Scope
 

From ecaf67c9305f3bf75a9798e8a2e10b36955df42c Mon Sep 17 00:00:00 2001
From: Eslam Salem <eslam@shieldfy.com>
Date: Mon, 11 Apr 2022 10:56:45 +0200
Subject: [PATCH 23/31] docs: remove Node Security Project from security policy

closes #4890
---
 Security.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/Security.md b/Security.md
index 858dfffc5b..cdcd7a6e0a 100644
--- a/Security.md
+++ b/Security.md
@@ -27,8 +27,7 @@ endeavor to keep you informed of the progress towards a fix and full
 announcement, and may ask for additional information or guidance.
 
 Report security bugs in third-party modules to the person or team maintaining
-the module. You can also report a vulnerability through the
-[Node Security Project](https://nodesecurity.io/report).
+the module.
 
 ## Disclosure Policy
 

From b91c7ffb289af1753b9d1d84e16fbfcd34954124 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Mon, 11 Apr 2022 19:28:50 -0400
Subject: [PATCH 24/31] examples: use http-errors to create errors

---
 examples/params/index.js | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/examples/params/index.js b/examples/params/index.js
index b153b93b98..b6fc483c8b 100644
--- a/examples/params/index.js
+++ b/examples/params/index.js
@@ -4,6 +4,7 @@
  * Module dependencies.
  */
 
+var createError = require('http-errors')
 var express = require('../../');
 var app = module.exports = express();
 
@@ -17,14 +18,6 @@ var users = [
   , { name: 'bandit' }
 ];
 
-// Create HTTP error
-
-function createError(status, message) {
-  var err = new Error(message);
-  err.status = status;
-  return err;
-}
-
 // Convert :to and :from to integers
 
 app.param(['to', 'from'], function(req, res, next, num, name){

From 8880ddad1c0f00612b53f5f686f55e7566b16562 Mon Sep 17 00:00:00 2001
From: Hashen <37979557+Hashen110@users.noreply.github.com>
Date: Thu, 7 Apr 2022 21:34:47 +0530
Subject: [PATCH 25/31] examples: add missing html label associations

closes #4884
---
 examples/auth/views/login.ejs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/examples/auth/views/login.ejs b/examples/auth/views/login.ejs
index 8a20411a2c..181c36caf7 100644
--- a/examples/auth/views/login.ejs
+++ b/examples/auth/views/login.ejs
@@ -6,12 +6,12 @@
 Try accessing <a href="/restricted">/restricted</a>, then authenticate with "tj" and "foobar".
 <form method="post" action="/login">
   <p>
-    <label>Username:</label>
-    <input type="text" name="username">
+    <label for="username">Username:</label>
+    <input type="text" name="username" id="username">
   </p>
   <p>
-    <label>Password:</label>
-    <input type="text" name="password">
+    <label for="password">Password:</label>
+    <input type="text" name="password" id="password">
   </p>
   <p>
     <input type="submit" value="Login">

From 92c5ce59f51cce4b3598fd040117772fac42dce8 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Mon, 11 Apr 2022 22:51:13 -0400
Subject: [PATCH 26/31] deps: cookie@0.5.0

---
 History.md         |  3 ++
 package.json       |  2 +-
 test/res.cookie.js | 72 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 76 insertions(+), 1 deletion(-)

diff --git a/History.md b/History.md
index 84efe9b4d8..4f43ad9255 100644
--- a/History.md
+++ b/History.md
@@ -19,6 +19,9 @@ unreleased
     - deps: on-finished@2.4.1
     - deps: qs@6.10.3
     - deps: raw-body@2.5.1
+  * deps: cookie@0.5.0
+    - Add `priority` option
+    - Fix `expires` option to reject invalid dates
   * 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 dfce12352c..ede86798cf 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
     "body-parser": "1.20.0",
     "content-disposition": "0.5.4",
     "content-type": "~1.0.4",
-    "cookie": "0.4.2",
+    "cookie": "0.5.0",
     "cookie-signature": "1.0.6",
     "debug": "2.6.9",
     "depd": "2.0.0",
diff --git a/test/res.cookie.js b/test/res.cookie.js
index e3a921301f..93deb76988 100644
--- a/test/res.cookie.js
+++ b/test/res.cookie.js
@@ -67,6 +67,21 @@ describe('res', function(){
       .expect(200, done)
     })
 
+    describe('expires', function () {
+      it('should throw on invalid date', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.cookie('name', 'tobi', { expires: new Date(NaN) })
+          res.end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(500, /option expires is invalid/, done)
+      })
+    })
+
     describe('maxAge', function(){
       it('should set relative expires', function(done){
         var app = express();
@@ -155,6 +170,63 @@ describe('res', function(){
       })
     })
 
+    describe('priority', function () {
+      it('should set low priority', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.cookie('name', 'tobi', { priority: 'low' })
+          res.end()
+        })
+
+        request(app)
+          .get('/')
+          .expect('Set-Cookie', /Priority=Low/)
+          .expect(200, done)
+      })
+
+      it('should set medium priority', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.cookie('name', 'tobi', { priority: 'medium' })
+          res.end()
+        })
+
+        request(app)
+          .get('/')
+          .expect('Set-Cookie', /Priority=Medium/)
+          .expect(200, done)
+      })
+
+      it('should set high priority', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.cookie('name', 'tobi', { priority: 'high' })
+          res.end()
+        })
+
+        request(app)
+          .get('/')
+          .expect('Set-Cookie', /Priority=High/)
+          .expect(200, done)
+      })
+
+      it('should throw with invalid priority', function (done) {
+        var app = express()
+
+        app.use(function (req, res) {
+          res.cookie('name', 'tobi', { priority: 'foobar' })
+          res.end()
+        })
+
+        request(app)
+          .get('/')
+          .expect(500, /option priority is invalid/, done)
+      })
+    })
+
     describe('signed', function(){
       it('should generate a signed JSON cookie', function(done){
         var app = express();

From 708ac4cdf5cd0a658d62490a9f4d78d3e1ec6612 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Wed, 13 Apr 2022 23:29:25 -0400
Subject: [PATCH 27/31] Fix handling very large stacks of sync middleware

closes #4891
---
 History.md          |  1 +
 lib/router/index.js |  8 ++++++++
 lib/router/route.js |  9 +++++++++
 test/Route.js       | 22 ++++++++++++++++++++++
 test/Router.js      | 16 ++++++++++++++++
 5 files changed, 56 insertions(+)

diff --git a/History.md b/History.md
index 4f43ad9255..3f7851ba57 100644
--- a/History.md
+++ b/History.md
@@ -5,6 +5,7 @@ unreleased
   * Allow `options` without `filename` in `res.download`
   * Deprecate string and non-integer arguments to `res.status`
   * Fix behavior of `null`/`undefined` as `maxAge` in `res.cookie`
+  * Fix handling very large stacks of sync middleware
   * Ignore `Object.prototype` values in settings through `app.set`/`app.get`
   * Invoke `default` with same arguments as types in `res.format`
   * Support proper 205 responses using `res.send`
diff --git a/lib/router/index.js b/lib/router/index.js
index 791a600f86..f4c8c0a79e 100644
--- a/lib/router/index.js
+++ b/lib/router/index.js
@@ -142,6 +142,7 @@ proto.handle = function handle(req, res, out) {
   var protohost = getProtohost(req.url) || ''
   var removed = '';
   var slashAdded = false;
+  var sync = 0
   var paramcalled = {};
 
   // store options for OPTIONS request
@@ -203,6 +204,11 @@ proto.handle = function handle(req, res, out) {
       return;
     }
 
+    // max sync stack
+    if (++sync > 100) {
+      return setImmediate(next, err)
+    }
+
     // get pathname of request
     var path = getPathname(req);
 
@@ -321,6 +327,8 @@ proto.handle = function handle(req, res, out) {
     } else {
       layer.handle_request(req, res, next);
     }
+
+    sync = 0
   }
 };
 
diff --git a/lib/router/route.js b/lib/router/route.js
index 178df0d516..5adaa125e2 100644
--- a/lib/router/route.js
+++ b/lib/router/route.js
@@ -98,6 +98,8 @@ Route.prototype._options = function _options() {
 Route.prototype.dispatch = function dispatch(req, res, done) {
   var idx = 0;
   var stack = this.stack;
+  var sync = 0
+
   if (stack.length === 0) {
     return done();
   }
@@ -127,6 +129,11 @@ Route.prototype.dispatch = function dispatch(req, res, done) {
       return done(err);
     }
 
+    // max sync stack
+    if (++sync > 100) {
+      return setImmediate(next, err)
+    }
+
     if (layer.method && layer.method !== method) {
       return next(err);
     }
@@ -136,6 +143,8 @@ Route.prototype.dispatch = function dispatch(req, res, done) {
     } else {
       layer.handle_request(req, res, next);
     }
+
+    sync = 0
   }
 };
 
diff --git a/test/Route.js b/test/Route.js
index 8e7ddbdbcc..3bdc8d7df2 100644
--- a/test/Route.js
+++ b/test/Route.js
@@ -13,6 +13,28 @@ describe('Route', function(){
     route.dispatch(req, {}, done)
   })
 
+  it('should not stack overflow with a large sync stack', function (done) {
+    this.timeout(5000) // long-running test
+
+    var req = { method: 'GET', url: '/' }
+    var route = new Route('/foo')
+
+    for (var i = 0; i < 6000; i++) {
+      route.all(function (req, res, next) { next() })
+    }
+
+    route.get(function (req, res, next) {
+      req.called = true
+      next()
+    })
+
+    route.dispatch(req, {}, function (err) {
+      if (err) return done(err)
+      assert.ok(req.called)
+      done()
+    })
+  })
+
   describe('.all', function(){
     it('should add handler', function(done){
       var req = { method: 'GET', url: '/' };
diff --git a/test/Router.js b/test/Router.js
index 907b972636..8a0654bca3 100644
--- a/test/Router.js
+++ b/test/Router.js
@@ -76,6 +76,22 @@ describe('Router', function(){
     router.handle({ url: '/', method: 'GET' }, { end: done });
   });
 
+  it('should not stack overflow with a large sync stack', function (done) {
+    this.timeout(5000) // long-running test
+
+    var router = new Router()
+
+    for (var i = 0; i < 6000; i++) {
+      router.use(function (req, res, next) { next() })
+    }
+
+    router.use(function (req, res) {
+      res.end()
+    })
+
+    router.handle({ url: '/', method: 'GET' }, { end: done })
+  })
+
   describe('.handle', function(){
     it('should dispatch', function(done){
       var router = new Router();

From fd8e45c344325a4a91c1b916f3617a3574018976 Mon Sep 17 00:00:00 2001
From: phoenix <felix.niederwanger@suse.com>
Date: Fri, 8 Apr 2022 10:31:27 +0200
Subject: [PATCH 28/31] tests: mark stack overflow as long running

closes #4887
---
 test/Router.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/test/Router.js b/test/Router.js
index 8a0654bca3..bf5a31ffdd 100644
--- a/test/Router.js
+++ b/test/Router.js
@@ -62,6 +62,8 @@ describe('Router', function(){
   })
 
   it('should not stack overflow with many registered routes', function(done){
+    this.timeout(5000) // long-running test
+
     var handler = function(req, res){ res.end(new Error('wrong handler')) };
     var router = new Router();
 

From 11a209e4b7e229bf5041e1ab76ba0ac4e0cad324 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Wed, 20 Apr 2022 22:02:37 -0400
Subject: [PATCH 29/31] build: support Node.js 17.x

---
 .github/workflows/ci.yml | 4 ++++
 appveyor.yml             | 1 +
 2 files changed, 5 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 83a8edee87..f4a5902ac9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,6 +29,7 @@ jobs:
         - Node.js 14.x
         - Node.js 15.x
         - Node.js 16.x
+        - Node.js 17.x
 
         include:
         - name: Node.js 0.10
@@ -98,6 +99,9 @@ jobs:
         - name: Node.js 16.x
           node-version: "16.14"
 
+        - name: Node.js 17.x
+          node-version: "17.9"
+
     steps:
     - uses: actions/checkout@v2
 
diff --git a/appveyor.yml b/appveyor.yml
index 93ea2c8161..f97addec1c 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -18,6 +18,7 @@ environment:
     - nodejs_version: "14.19"
     - nodejs_version: "15.14"
     - nodejs_version: "16.14"
+    - nodejs_version: "17.9"
 cache:
   - node_modules
 install:

From 29ea1b2f74c5e76e79e329ef425e5fbbcd6a71c3 Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Thu, 21 Apr 2022 01:38:59 -0400
Subject: [PATCH 30/31] build: use 64-bit Node.js in AppVeyor

---
 appveyor.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/appveyor.yml b/appveyor.yml
index f97addec1c..faf31abf12 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -25,7 +25,7 @@ install:
   # Install Node.js
   - ps: >-
       try { Install-Product node $env:nodejs_version -ErrorAction Stop }
-      catch { Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) }
+      catch { Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) x64 }
   # Configure npm
   - ps: |
       npm config set loglevel error

From 158a17031a2668269aedb31ea07b58d6b700272b Mon Sep 17 00:00:00 2001
From: Douglas Christopher Wilson <doug@somethingdoug.com>
Date: Thu, 21 Apr 2022 02:09:08 -0400
Subject: [PATCH 31/31] build: support Node.js 18.x

---
 .github/workflows/ci.yml | 4 ++++
 appveyor.yml             | 1 +
 2 files changed, 5 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f4a5902ac9..9d3663762c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,6 +30,7 @@ jobs:
         - Node.js 15.x
         - Node.js 16.x
         - Node.js 17.x
+        - Node.js 18.x
 
         include:
         - name: Node.js 0.10
@@ -102,6 +103,9 @@ jobs:
         - name: Node.js 17.x
           node-version: "17.9"
 
+        - name: Node.js 18.x
+          node-version: "18.0"
+
     steps:
     - uses: actions/checkout@v2
 
diff --git a/appveyor.yml b/appveyor.yml
index faf31abf12..8804cfd398 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -19,6 +19,7 @@ environment:
     - nodejs_version: "15.14"
     - nodejs_version: "16.14"
     - nodejs_version: "17.9"
+    - nodejs_version: "18.0"
 cache:
   - node_modules
 install: