From c2609ec2e5ed0c90e2fb77b3ac592af99f98a58a Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Wed, 24 Jun 2026 20:29:51 -0500 Subject: [PATCH] feat: implement support for 2d duplicate headers in writeHead --- index.js | 28 ++++++++++++++++++++++- test/test.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index d691cc7..8c2b7d4 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,16 @@ var http = require('http') // older node versions don't have appendHeader var isAppendHeaderSupported = typeof http.ServerResponse.prototype.appendHeader === 'function' + +// node v22 (https://github.com/nodejs/node/pull/50394) made writeHead preserve +// duplicate headers passed as a raw array even when setHeader was called before; +// earlier versions collapse them to the last value. since this monkey-patch +// always applies headers via setHeader/appendHeader, only preserve duplicates on +// versions where node itself does, so behavior stays consistent with the runtime +var preservesDuplicateHeaders = isAppendHeaderSupported && parseInt(process.versions.node, 10) >= 22 + var set1dArray = isAppendHeaderSupported ? set1dArrayWithAppend : set1dArrayWithSet +var set2dArray = preservesDuplicateHeaders ? set2dArrayWithAppend : set2dArrayWithSet /** * Create a replacement writeHead method. @@ -145,7 +154,24 @@ function setWriteHeadHeaders (statusCode) { return args } -function set2dArray (res, headers) { +function set2dArrayWithAppend (res, headers) { + var key + for (var i = 0; i < headers.length; i++) { + key = headers[i][0] + if (key) { + res.removeHeader(key) + } + } + + for (var j = 0; j < headers.length; j++) { + key = headers[j][0] + if (key) { + res.appendHeader(key, headers[j][1]) + } + } +} + +function set2dArrayWithSet (res, headers) { var key for (var i = 0; i < headers.length; i++) { key = headers[i][0] diff --git a/test/test.js b/test/test.js index 2efcf30..23d343e 100644 --- a/test/test.js +++ b/test/test.js @@ -7,6 +7,10 @@ var request = require('supertest') // older node versions don't have appendHeader var isAppendHeaderSupported = typeof http.ServerResponse.prototype.appendHeader === 'function' +// node v22 (https://github.com/nodejs/node/pull/50394) preserves duplicate +// headers from a raw array passed to writeHead; earlier versions collapse them +var preservesDuplicateHeaders = isAppendHeaderSupported && parseInt(process.versions.node, 10) >= 22 + describe('onHeaders(res, listener)', function () { it('should fire after setHeader', function (done) { var server = createServer(echoListener) @@ -429,6 +433,66 @@ describe('onHeaders(res, listener)', function () { }) }) }) + + describe('writeHead(status, 2d duplicate headers)', function () { + it('should be respected', function (done) { + var server = createServer(listener, handler) + + function handler (req, res) { + res.writeHead(201, [['express', 'is good'], ['express', 'is great']]) + } + + function listener (req, res) { + // no need to duplicate existing listener tests further... right? + } + + var response = request(server).get('/') + + if (preservesDuplicateHeaders) { + response + .expect('express', 'is good, is great') + } else { + response + .expect('express', 'is great') + } + + response + .expect(201) + .end(function (err, res) { + if (err) throw err + + var expressIsGood = false + var expressIsGreat = false + + // very old node versions do not have the `rawHeaders` prop + var headers = res.res.rawHeaders || res.res.headers + + if (headers.length) { + for (var i = 0; i < headers.length; i++) { + const header = headers[i] + + if (header === 'express') { + if (headers[i + 1] === 'is good') { + expressIsGood = true + } else if (headers[i + 1] === 'is great') { + expressIsGreat = true + } + } + } + } else { + expressIsGreat = headers.express === 'is great' + } + + if (preservesDuplicateHeaders) { + assert.ok(expressIsGood) + } + + assert.ok(expressIsGreat) + + done() + }) + }) + }) }) function createServer (listener, handler) {