From 484e598e55b4e1430e64db22b61c1099cc8e340a Mon Sep 17 00:00:00 2001 From: Vansh Sharma Date: Tue, 24 Mar 2026 22:57:09 +0530 Subject: [PATCH] fix: preserve writeHead statusText and headers when compression is active When a user calls res.writeHead(200, undefined, {'foo': 'bar'}), the compression middleware was overwriting those custom headers because: 1. on-headers drops headers when statusText is undefined (treats args[1] as undefined, not looking at args[2]) 2. compression calls this.writeHead(this.statusCode) without the original statusText/headers, losing them Fix: intercept res.writeHead to capture user-supplied args and replay them in res.write/res.end instead of calling writeHead with only the status code. This avoids relying on private res._header property. Fixes #254 --- index.js | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index c0638e08..90fd6b51 100644 --- a/index.js +++ b/index.js @@ -40,10 +40,10 @@ var hasBrotliSupport = 'createBrotliCompress' in zlib * Module variables. * @private */ + var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/ var SUPPORTED_ENCODING = hasBrotliSupport ? ['br', 'gzip', 'deflate', 'identity'] : ['gzip', 'deflate', 'identity'] var PREFERRED_ENCODING = hasBrotliSupport ? ['br', 'gzip'] : ['gzip'] - var encodingSupported = ['gzip', 'deflate', 'identity', 'br'] /** @@ -60,10 +60,8 @@ function compression (options) { if (hasBrotliSupport) { Object.assign(optsBrotli, opts.brotli) - var brotliParams = {} brotliParams[zlib.constants.BROTLI_PARAM_QUALITY] = 4 - // set the default level to a reasonable value with balanced speed/ratio optsBrotli.params = Object.assign(brotliParams, optsBrotli.params) } @@ -86,6 +84,21 @@ function compression (options) { var _end = res.end var _on = res.on var _write = res.write + var _writeHead = res.writeHead + + // track whether user explicitly called writeHead with custom statusText/headers + var userWriteHeadArgs = null + + // intercept writeHead to capture user-supplied statusText and headers + // this is needed because on-headers drops the headers object when statusText is undefined + // see: https://github.com/expressjs/compression/issues/254 + res.writeHead = function writeHead (statusCode) { + // only capture on first explicit user call (before on-headers patches it further) + if (arguments.length >= 2 && userWriteHeadArgs === null) { + userWriteHeadArgs = Array.prototype.slice.call(arguments) + } + return _writeHead.apply(this, arguments) + } // flush res.flush = function flush () { @@ -95,14 +108,19 @@ function compression (options) { } // proxy - res.write = function write (chunk, encoding) { if (ended) { return false } if (!headersSent(res)) { - this.writeHead(this.statusCode) + // If user explicitly called writeHead, let those headers be applied via on-headers + // otherwise trigger writeHead ourselves to fire the on-headers listener + if (!userWriteHeadArgs) { + this.writeHead(this.statusCode) + } else { + this.writeHead.apply(this, userWriteHeadArgs) + } } return stream @@ -121,7 +139,14 @@ function compression (options) { length = chunkLength(chunk, encoding) } - this.writeHead(this.statusCode) + // If user explicitly called writeHead, replay those args so statusText + // and custom headers are preserved (works around on-headers bug with + // undefined statusText: https://github.com/expressjs/compression/issues/254) + if (!userWriteHeadArgs) { + this.writeHead(this.statusCode) + } else { + this.writeHead.apply(this, userWriteHeadArgs) + } } if (!stream) {