Skip to content

Commit 7b5ce83

Browse files
authored
fix: handle HTTP/2 requests with pseudo-headers (#534)
HTTP/2 requests are required to have a specific set of request and response "pseudo-headers": https://www.rfc-editor.org/rfc/rfc9113.html#name-request-pseudo-header-field. When converting a Node.js `IncomingMessage` to a Web `Request`, we must ignore these pseudo-headers because `IncomingMessage.prototype.headers` *does* contain them, but newer versions of Node.js will throw on `new Request()` if given any such header.
1 parent c903969 commit 7b5ce83

File tree

2 files changed

+62
-3
lines changed

2 files changed

+62
-3
lines changed

packages/dev/src/lib/reqres.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1-
import { IncomingHttpHeaders, IncomingMessage } from 'node:http'
1+
import { IncomingMessage } from 'node:http'
22
import { Readable } from 'node:stream'
33

4-
export const normalizeHeaders = (headers: IncomingHttpHeaders): HeadersInit => {
4+
export const normalizeHeaders = (request: IncomingMessage) => {
55
const result: [string, string][] = []
6+
let headers = request.headers
7+
8+
// Handle HTTP/2 pseudo-headers: https://www.rfc-editor.org/rfc/rfc9113.html#name-request-pseudo-header-field
9+
// In certain versions of Node.js, the built-in `Request` constructor from undici throws
10+
// if a header starts with a colon.
11+
if (request.httpVersionMajor >= 2) {
12+
headers = { ...headers }
13+
delete headers[':authority']
14+
delete headers[':method']
15+
delete headers[':path']
16+
delete headers[':scheme']
17+
}
618

719
for (const [key, value] of Object.entries(headers)) {
820
if (Array.isArray(value)) {
@@ -43,11 +55,14 @@ export const getNormalizedRequestFromNodeRequest = (
4355
? null
4456
: (Readable.toWeb(input) as unknown as ReadableStream<unknown>)
4557

58+
const normalizedHeaders = normalizeHeaders(input)
59+
normalizedHeaders.push(['x-nf-request-id', requestID])
60+
4661
return new Request(fullUrl, {
4762
body,
4863
// @ts-expect-error Not typed!
4964
duplex: 'half',
50-
headers: normalizeHeaders({ ...input.headers, 'x-nf-request-id': requestID }),
65+
headers: normalizedHeaders,
5166
method,
5267
})
5368
}

packages/dev/src/main.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { readFile } from 'node:fs/promises'
2+
import { IncomingMessage } from 'node:http'
3+
import { Socket } from 'node:net'
24
import { resolve } from 'node:path'
35

46
import { createImageServerHandler, Fixture, generateImage, getImageResponseSize, HTTPServer } from '@netlify/dev-utils'
@@ -14,6 +16,48 @@ describe('Handling requests', () => {
1416
vi.unstubAllEnvs()
1517
})
1618

19+
test('Handles HTTP/2 Node.js requests', async () => {
20+
const fixture = new Fixture()
21+
.withFile(
22+
'netlify.toml',
23+
`[build]
24+
publish = "public"
25+
`,
26+
)
27+
.withFile('public/index.html', 'Hello from static file')
28+
const directory = await fixture.create()
29+
const dev = new NetlifyDev({
30+
projectRoot: directory,
31+
geolocation: { enabled: false },
32+
})
33+
await dev.start()
34+
35+
const nodeReq = new IncomingMessage(new Socket())
36+
nodeReq.httpVersionMajor = 2
37+
nodeReq.httpVersionMinor = 0
38+
nodeReq.method = 'GET'
39+
nodeReq.url = '/index.html'
40+
nodeReq.headers = {
41+
accept: 'text/html',
42+
host: 'example.netlify.app',
43+
'user-agent': 'test-agent',
44+
// These four HTTP/2 pseudo request headers are required per the HTTP/2 spec:
45+
// https://www.rfc-editor.org/rfc/rfc9113.html#name-request-pseudo-header-field
46+
// These show up here like any other header on Node.js IncomingMessage objects,
47+
':method': 'GET',
48+
':path': '/index.html',
49+
':scheme': 'https',
50+
':authority': 'example.netlify.app',
51+
}
52+
const result = await dev.handleAndIntrospectNodeRequest(nodeReq)
53+
54+
expect(result?.response.ok).toBe(true)
55+
expect(await result?.response.text()).toBe('Hello from static file')
56+
57+
await dev.stop()
58+
await fixture.destroy()
59+
})
60+
1761
describe('No linked site', () => {
1862
test('Same-site rewrite to a static file', async () => {
1963
const fixture = new Fixture()

0 commit comments

Comments
 (0)