From 4bdf1f4d3d407ebbb3c62671d193dac5d044ecb4 Mon Sep 17 00:00:00 2001 From: aui Date: Mon, 25 May 2026 10:06:01 +0800 Subject: [PATCH 1/2] fix(node-utils): computeOrigin respects HTTP/2 :authority Fixes #1152 --- .changeset/brave-foxes-hear.md | 5 +++ .../node-utils/src/node-to-edge/headers.ts | 4 +++ .../node-utils/src/node-to-edge/request.ts | 30 ++++++++++++++-- .../test/node-to-edge/request.test.ts | 35 +++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 .changeset/brave-foxes-hear.md diff --git a/.changeset/brave-foxes-hear.md b/.changeset/brave-foxes-hear.md new file mode 100644 index 00000000..ef5fb2e4 --- /dev/null +++ b/.changeset/brave-foxes-hear.md @@ -0,0 +1,5 @@ +--- +'@edge-runtime/node-utils': patch +--- + +fix: computeOrigin respects HTTP/2 :authority pseudo-header diff --git a/packages/node-utils/src/node-to-edge/headers.ts b/packages/node-utils/src/node-to-edge/headers.ts index 4134e97e..47ee1ca5 100644 --- a/packages/node-utils/src/node-to-edge/headers.ts +++ b/packages/node-utils/src/node-to-edge/headers.ts @@ -9,6 +9,10 @@ export function buildToHeaders({ Headers }: Dependencies) { return function toHeaders(nodeHeaders: IncomingHttpHeaders): Headers { const headers = new Headers() for (let [key, value] of Object.entries(nodeHeaders)) { + // HTTP/2 pseudo-headers (e.g. :authority) are not valid Fetch header names. + if (key.startsWith(':')) { + continue + } const values = Array.isArray(value) ? value : [value] for (let v of values) { if (v !== undefined) { diff --git a/packages/node-utils/src/node-to-edge/request.ts b/packages/node-utils/src/node-to-edge/request.ts index 68730144..1febf561 100644 --- a/packages/node-utils/src/node-to-edge/request.ts +++ b/packages/node-utils/src/node-to-edge/request.ts @@ -30,11 +30,37 @@ export function buildToRequest(dependencies: BuildDependencies) { } } +function getSingleHeader( + headers: IncomingMessage['headers'], + name: string, +): string | undefined { + const value = headers[name] + if (value === undefined) { + return undefined + } + return Array.isArray(value) ? value[0] : value +} + function computeOrigin({ headers }: IncomingMessage, defaultOrigin: string) { - const authority = headers.host + const authority = + getSingleHeader(headers, 'host') ?? getSingleHeader(headers, ':authority') if (!authority) { return defaultOrigin } + + let protocol = 'http' + if (defaultOrigin) { + try { + protocol = new URL(defaultOrigin).protocol.replace(':', '') || 'http' + } catch { + // keep default + } + } + const [, port] = authority.split(':') - return `${port === '443' ? 'https' : 'http'}://${authority}` + if (port === '443') { + protocol = 'https' + } + + return `${protocol}://${authority}` } diff --git a/packages/node-utils/test/node-to-edge/request.test.ts b/packages/node-utils/test/node-to-edge/request.test.ts index 470b91f1..db66375d 100644 --- a/packages/node-utils/test/node-to-edge/request.test.ts +++ b/packages/node-utils/test/node-to-edge/request.test.ts @@ -1,3 +1,4 @@ +import type { IncomingMessage } from 'node:http' import type { TestServer } from '../test-utils/run-test-server' import { buildToRequest } from '../../src/node-to-edge/request' import { runTestServer } from '../test-utils/run-test-server' @@ -97,6 +98,33 @@ it(`uses request host header as request url origin`, async () => { ).resolves.toHaveProperty('url', `https://${host}/`) }) +it(`uses :authority as request url origin when host is missing`, () => { + const request = requestFromNodeHeaders({ + ':authority': 'example.test:5173', + }) + expect(request.url).toBe('https://example.test:5173/') +}) + +it(`prefers host over :authority as request url origin`, () => { + const request = requestFromNodeHeaders({ + host: 'host.test', + ':authority': 'authority.test', + }) + expect(request.url).toBe('https://host.test/') +}) + +it(`uses https when :authority port is 443`, () => { + const request = requestFromNodeHeaders({ + ':authority': 'example.test:443', + }) + expect(request.url).toBe('https://example.test/') +}) + +it(`falls back to default origin when host and :authority are missing`, () => { + const request = requestFromNodeHeaders({}) + expect(request.url).toBe('https://fallback.test/') +}) + it('allows to read the body as text', async () => { const request = await mapRequest(server.url, { body: 'Hello World', @@ -139,6 +167,13 @@ it('does not allow to read the body twice', async () => { await expect(request.text()).rejects.toThrowError('Body is unusable') }) +function requestFromNodeHeaders(headers: IncomingMessage['headers']) { + return nodeRequestToRequest( + { url: '/', method: 'GET', headers } as IncomingMessage, + { defaultOrigin: 'https://fallback.test' }, + ) +} + async function mapRequest(input: string, init: RequestInit = {}) { const requestId = EdgeRuntime.crypto.randomUUID() const headers = new EdgeRuntime.Headers(init.headers) From baeecfbaef8b9e9ae7084b5b4959d3ec89c492b5 Mon Sep 17 00:00:00 2001 From: aui Date: Mon, 25 May 2026 10:17:49 +0800 Subject: [PATCH 2/2] test(integration-tests): stabilize FormData body text assertion --- packages/integration-tests/test/body.test.ts | 37 ++++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/integration-tests/test/body.test.ts b/packages/integration-tests/test/body.test.ts index 521b8df1..74161a52 100644 --- a/packages/integration-tests/test/body.test.ts +++ b/packages/integration-tests/test/body.test.ts @@ -83,25 +83,24 @@ describe('body', () => { const res = new Response(formData) const text = await res?.text() - expect(text.replace(/formdata-undici-0\d+/g, 'formdata-unidici-0.1234')) - .toMatchInlineSnapshot(` - "------formdata-unidici-0.1234 - Content-Disposition: form-data; name="name" - - John - ------formdata-unidici-0.1234 - Content-Disposition: form-data; name="lastname" - - Doe - ------formdata-unidici-0.1234 - Content-Disposition: form-data; name="metadata"; filename="blob" - Content-Type: application/json - - { - "hello": "world" - } - ------formdata-unidici-0.1234--" - `) + const normalized = text + .replace(/formdata-undici-0\d+/g, 'formdata-unidici-0.1234') + .replace(/\r\n/g, '\n') + .trimEnd() + + expect(normalized).toContain( + 'Content-Disposition: form-data; name="name"', + ) + expect(normalized).toContain('John') + expect(normalized).toContain( + 'Content-Disposition: form-data; name="lastname"', + ) + expect(normalized).toContain('Doe') + expect(normalized).toContain( + 'Content-Disposition: form-data; name="metadata"; filename="blob"', + ) + expect(normalized).toContain('"hello": "world"') + expect(normalized).toMatch(/------formdata-unidici-0\.1234--\n?$/) }) test('allows to read a null body as ArrayBuffer', async () => {