From 7ecea4c2ff70a649b9c111c8eba378d27d3ed50d Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 3 Jun 2026 09:49:22 +0200 Subject: [PATCH] fix: preserve raw headers in dispatch handler bridge Signed-off-by: Matteo Collina --- lib/handler/unwrap-handler.js | 6 +++ test/node-test/global-dispatcher-version.js | 49 +++++++++++++++++++++ types/dispatcher.d.ts | 2 + 3 files changed, 57 insertions(+) diff --git a/lib/handler/unwrap-handler.js b/lib/handler/unwrap-handler.js index e23b9666cf7..2949faea0fe 100644 --- a/lib/handler/unwrap-handler.js +++ b/lib/handler/unwrap-handler.js @@ -13,6 +13,9 @@ class UnwrapController { [kResume] = null + rawHeaders = null + rawTrailers = null + constructor (abort) { this.#abort = abort } @@ -72,11 +75,13 @@ module.exports = class UnwrapHandler { } onUpgrade (statusCode, rawHeaders, socket) { + this.#controller.rawHeaders = rawHeaders this.#handler.onRequestUpgrade?.(this.#controller, statusCode, parseHeaders(rawHeaders), socket) } onHeaders (statusCode, rawHeaders, resume, statusMessage) { this.#controller[kResume] = resume + this.#controller.rawHeaders = rawHeaders this.#handler.onResponseStart?.(this.#controller, statusCode, parseHeaders(rawHeaders), statusMessage) return !this.#controller.paused } @@ -87,6 +92,7 @@ module.exports = class UnwrapHandler { } onComplete (rawTrailers) { + this.#controller.rawTrailers = rawTrailers this.#handler.onResponseEnd?.(this.#controller, parseHeaders(rawTrailers)) } diff --git a/test/node-test/global-dispatcher-version.js b/test/node-test/global-dispatcher-version.js index ed4c0a2e18a..b9b594eb3a0 100644 --- a/test/node-test/global-dispatcher-version.js +++ b/test/node-test/global-dispatcher-version.js @@ -93,3 +93,52 @@ test('setGlobalDispatcher mirrors the dispatcher under the v1 symbol that Node.j assert.strictEqual(payload.mirroredV1, true) assert.strictEqual(payload.mirroredV2, true) }) + +test('Node.js global fetch preserves headers and decoding with an undici Agent dispatcher', () => { + const script = ` + const assert = require('node:assert') + const { createServer } = require('node:http') + const { once } = require('node:events') + const { brotliCompressSync } = require('node:zlib') + const { Agent } = require('./index.js') + + ;(async () => { + const body = Buffer.from('body content') + const compressedBody = brotliCompressSync(body) + const server = createServer((_request, response) => { + response.writeHead(200, { + 'content-type': 'application/x-ndjson', + 'content-encoding': 'br', + 'another-test-header': 'test-value' + }) + response.end(compressedBody) + }) + server.listen(0) + await once(server, 'listening') + + const url = 'http://127.0.0.1:' + server.address().port + const cases = [ + ['global dispatcher', {}], + ['custom dispatcher', { dispatcher: new Agent() }] + ] + + for (const [label, init] of cases) { + const response = await fetch(url, init) + const responseBody = Buffer.from(await response.arrayBuffer()).toString('utf8') + + assert.strictEqual(response.headers.get('content-type'), 'application/x-ndjson', label) + assert.strictEqual(response.headers.get('content-encoding'), 'br', label) + assert.strictEqual(response.headers.get('another-test-header'), 'test-value', label) + assert.strictEqual(responseBody, 'body content', label) + } + + server.close() + })().catch((err) => { + console.error(err?.cause?.stack || err?.stack || err) + process.exit(1) + }) + ` + + const result = runNode(script) + assert.strictEqual(result.status, 0, result.stderr) +}) diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 33dc6051626..3f2ec3d498b 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -212,6 +212,8 @@ declare namespace Dispatcher { get aborted(): boolean get paused(): boolean get reason(): Error | null + rawHeaders?: Buffer[] | string[] | IncomingHttpHeaders | null + rawTrailers?: Buffer[] | string[] | IncomingHttpHeaders | null abort(reason: Error): void pause(): void resume(): void