From 94b4f691cf05d72c1b978ad6e8af822fb4f999fd Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sun, 27 Sep 2026 11:18:52 +0700 Subject: [PATCH] fix(fetch): skip HTTP/2 pseudo-headers in toFetchHeaders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node's http2 request headers include `:method`, `:path`, `:scheme` and `:authority`. These are not header fields (RFC 9113 §8.3) and fetch rejects their names, so converting a node http2 request's standard headers with toFetchHeaders threw `":path" is an invalid header name`. They are now skipped. --- packages/fetch/src/headers.test.ts | 16 ++++++++++++++++ packages/fetch/src/headers.ts | 8 ++++++++ tests/data-transfer.test.ts | 3 +++ 3 files changed, 27 insertions(+) diff --git a/packages/fetch/src/headers.test.ts b/packages/fetch/src/headers.test.ts index 21847a73..f069ff60 100644 --- a/packages/fetch/src/headers.test.ts +++ b/packages/fetch/src/headers.test.ts @@ -49,3 +49,19 @@ it('toFetchHeaders', () => { ['x-custom-header', 'custom-value'], ]) }) + +it('toFetchHeaders skips http2 pseudo-headers', () => { + const standardHeaders: StandardHeaders = { + ':method': 'POST', + ':path': '/hello?foo=bar', + ':scheme': 'https', + ':authority': 'example.com', + 'content-type': 'application/json', + } + + const fetchHeaders = toFetchHeaders(standardHeaders) + + expect([...fetchHeaders]).toEqual([ + ['content-type', 'application/json'], + ]) +}) diff --git a/packages/fetch/src/headers.ts b/packages/fetch/src/headers.ts index 60dc2984..817a50e2 100644 --- a/packages/fetch/src/headers.ts +++ b/packages/fetch/src/headers.ts @@ -25,11 +25,19 @@ export function toStandardHeaders(headers: Headers): StandardHeaders { /** * Convert standard headers to fetch headers. + * + * HTTP/2 pseudo-headers (`:method`, `:path`, ...), which node's http2 request + * headers carry, are skipped: they are not header fields (RFC 9113 §8.3) and + * fetch rejects their names. */ export function toFetchHeaders(standardHeaders: StandardHeaders): Headers { const headers = new Headers() for (const key of Object.keys(standardHeaders)) { + if (key.startsWith(':')) { + continue + } + const value = standardHeaders[key] if (Array.isArray(value)) { for (const v of value) { diff --git a/tests/data-transfer.test.ts b/tests/data-transfer.test.ts index f44f25fc..a5174487 100644 --- a/tests/data-transfer.test.ts +++ b/tests/data-transfer.test.ts @@ -1,4 +1,5 @@ import { ErrorEvent, unwrapEvent, withEventMeta } from '@standard-server/core' +import { toFetchHeaders } from '@standard-server/fetch' import { isAsyncIteratorObject, sleep } from '@standard-server/shared' import { expectPeerMessages } from './client-server' import { createExpressjsClientServerTest } from './client-server.expressjs' @@ -87,6 +88,8 @@ describe.each([ ...headers, 'x-from': 'client', }) + // node http2 request headers carry pseudo-headers, which fetch rejects + expect(toFetchHeaders(request.headers).get('x-from')).toEqual('client') }) it.each([