Skip to content

Commit dcf9d03

Browse files
committed
fix: handle invalid HTTP/2 headers gracefully (#4356)
When session.request() throws synchronously due to invalid HTTP/2 headers (e.g., duplicate headers like 'content-type' and 'Content-Type'), the error was not being caught, causing an uncaught exception. The fix wraps all session.request() calls with try-catch blocks. When an error is caught, only the request is errored (via util.errorRequest) and the function returns false. The session is not destroyed because the error is caused by the client sending invalid headers, not the server. This allows the client to continue using the same session for subsequent requests, and the error is properly caught by user code. Closes #4356
1 parent 77b42a3 commit dcf9d03

2 files changed

Lines changed: 75 additions & 19 deletions

File tree

lib/dispatcher/client-h2.js

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -531,14 +531,11 @@ function writeH2 (client, request) {
531531
try {
532532
stream = session.request(headers, { endStream: false, signal })
533533
} catch (err) {
534-
// An error here means the server sent invalid HTTP/2 headers
535-
// (e.g., HTTP/1 headers like "http2-settings"). This is a server error.
536-
// We destroy the socket/session and error the request so the client
537-
// can retry with a new connection.
534+
// An error here means the client sent invalid HTTP/2 headers
535+
// (e.g., duplicate headers like "content-type" and "Content-Type").
536+
// This is a client-side error, not a server-side error.
537+
// We only error the request and do not destroy the session.
538538
util.errorRequest(client, request, err)
539-
const socket = session[kSocket]
540-
session.destroy(err)
541-
util.destroy(socket, err)
542539
return false
543540
}
544541

@@ -579,11 +576,11 @@ function writeH2 (client, request) {
579576
try {
580577
stream = session.request(headers, { endStream: false, signal })
581578
} catch (err) {
582-
// An error here means the server sent invalid HTTP/2 headers
579+
// An error here means the client sent invalid HTTP/2 headers
580+
// (e.g., duplicate headers like "content-type" and "Content-Type").
581+
// This is a client-side error, not a server-side error.
582+
// We only error the request and do not destroy the session.
583583
util.errorRequest(client, request, err)
584-
const socket = session[kSocket]
585-
session.destroy(err)
586-
util.destroy(socket, err)
587584
return false
588585
}
589586
stream[kHTTP2Stream] = true
@@ -686,11 +683,11 @@ function writeH2 (client, request) {
686683
try {
687684
stream = session.request(headers, { endStream: shouldEndStream, signal })
688685
} catch (err) {
689-
// An error here means the server sent invalid HTTP/2 headers
686+
// An error here means the client sent invalid HTTP/2 headers
687+
// (e.g., duplicate headers like "content-type" and "Content-Type").
688+
// This is a client-side error, not a server-side error.
689+
// We only error the request and do not destroy the session.
690690
util.errorRequest(client, request, err)
691-
const socket = session[kSocket]
692-
session.destroy(err)
693-
util.destroy(socket, err)
694691
return false
695692
}
696693
stream[kHTTP2Stream] = true
@@ -703,11 +700,11 @@ function writeH2 (client, request) {
703700
signal
704701
})
705702
} catch (err) {
706-
// An error here means the server sent invalid HTTP/2 headers
703+
// An error here means the client sent invalid HTTP/2 headers
704+
// (e.g., duplicate headers like "content-type" and "Content-Type").
705+
// This is a client-side error, not a server-side error.
706+
// We only error the request and do not destroy the session.
707707
util.errorRequest(client, request, err)
708-
const socket = session[kSocket]
709-
session.destroy(err)
710-
util.destroy(socket, err)
711708
return false
712709
}
713710
stream[kHTTP2Stream] = true

test/http2-invalid-headers.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,62 @@ test('HTTP/2 invalid headers should be recoverable (#4356)', async t => {
5555
t.ok(client instanceof Client, 'client should still be a valid Client instance')
5656
t.ok(client.destroyed !== undefined, 'client state should be consistent')
5757
})
58+
59+
test('HTTP/2 duplicate headers should be catchable (#4356)', async t => {
60+
t = tspl(t, { plan: 3 })
61+
62+
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
63+
64+
let requestCount = 0
65+
server.on('stream', (stream, headers) => {
66+
requestCount++
67+
stream.respond({
68+
':status': 200,
69+
'content-type': 'text/plain'
70+
})
71+
stream.end('response')
72+
})
73+
74+
after(() => server.close())
75+
await once(server.listen(0), 'listening')
76+
77+
const client = new Client(`https://localhost:${server.address().port}`, {
78+
connect: {
79+
rejectUnauthorized: false
80+
},
81+
allowH2: true
82+
})
83+
after(() => client.close())
84+
85+
// Request with duplicate headers (content-type and Content-Type)
86+
// should throw a catchable error, not an uncaughtException
87+
try {
88+
await client.request({
89+
path: '/',
90+
method: 'POST',
91+
headers: {
92+
'content-type': 'foo/bar',
93+
'Content-Type': 'foo/bar'
94+
},
95+
body: 'foo-bar'
96+
})
97+
t.fail('should have thrown')
98+
} catch (err) {
99+
t.ok(err.code === 'ERR_HTTP2_INVALID_CONNECTION_HEADERS' ||
100+
err.code === 'ERR_HTTP2_HEADER_SINGLE_VALUE' ||
101+
err.code === 'UND_ERR_INFO',
102+
`expected ERR_HTTP2_INVALID_CONNECTION_HEADERS/ERR_HTTP2_HEADER_SINGLE_VALUE or UND_ERR_INFO, got ${err.code}`)
103+
}
104+
105+
// Verify the client is still functional (not crashed)
106+
t.ok(client instanceof Client, 'client should still be a valid Client instance')
107+
108+
// After the error, the client should be able to make another request
109+
// (on a new connection if the session was destroyed)
110+
const response = await client.request({
111+
path: '/',
112+
method: 'GET'
113+
})
114+
await response.body.text()
115+
t.strictEqual(requestCount, 2) // Two successful requests
116+
})

0 commit comments

Comments
 (0)