Skip to content

Commit cd388b9

Browse files
authored
fix(h2): honour headersTimeout (#5604)
1 parent 8d347dd commit cd388b9

3 files changed

Lines changed: 100 additions & 7 deletions

File tree

‎lib/dispatcher/client-h2.js‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ const {
88
RequestAbortedError,
99
SocketError,
1010
InformationalError,
11-
InvalidArgumentError
11+
InvalidArgumentError,
12+
HeadersTimeoutError,
13+
BodyTimeoutError
1214
} = require('../core/errors.js')
1315
const {
1416
kUrl,
@@ -33,6 +35,7 @@ const {
3335
kHTTPContext,
3436
kClosed,
3537
kBodyTimeout,
38+
kHeadersTimeout,
3639
kEnableConnectProtocol,
3740
kRemoteSettings,
3841
kHTTP2Stream,
@@ -416,7 +419,10 @@ function shouldSendContentLength (method) {
416419
}
417420

418421
function writeH2 (client, request) {
419-
const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout]
422+
// Time to the response headers, then time between body chunks. Using
423+
// bodyTimeout for both made headersTimeout a no-op over HTTP/2.
424+
const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout]
425+
const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout]
420426
const session = client[kHTTP2Session]
421427
const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
422428
let { body } = request
@@ -554,7 +560,7 @@ function writeH2 (client, request) {
554560
if (session[kOpenStreams] === 0) session.unref()
555561
})
556562

557-
stream.setTimeout(requestTimeout)
563+
stream.setTimeout(headersTimeout)
558564
return true
559565
}
560566

@@ -576,7 +582,7 @@ function writeH2 (client, request) {
576582
session[kOpenStreams] -= 1
577583
if (session[kOpenStreams] === 0) session.unref()
578584
})
579-
stream.setTimeout(requestTimeout)
585+
stream.setTimeout(headersTimeout)
580586

581587
return true
582588
}
@@ -677,7 +683,7 @@ function writeH2 (client, request) {
677683

678684
// Increment counter as we have new streams open
679685
++session[kOpenStreams]
680-
stream.setTimeout(requestTimeout)
686+
stream.setTimeout(headersTimeout)
681687

682688
// Track whether we received a response (headers)
683689
let responseReceived = false
@@ -686,6 +692,7 @@ function writeH2 (client, request) {
686692
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
687693
request.onResponseStarted()
688694
responseReceived = true
695+
stream.setTimeout(bodyTimeout)
689696

690697
// Due to the stream nature, it is possible we face a race condition
691698
// where the stream has been assigned, but the request has been aborted
@@ -755,7 +762,9 @@ function writeH2 (client, request) {
755762
})
756763

757764
stream.on('timeout', () => {
758-
const err = new InformationalError(`HTTP/2: "stream timeout after ${requestTimeout}"`)
765+
const err = responseReceived
766+
? new BodyTimeoutError(`HTTP/2: "body timeout after ${bodyTimeout}"`)
767+
: new HeadersTimeoutError(`HTTP/2: "headers timeout after ${headersTimeout}"`)
759768
stream.removeAllListeners('data')
760769
session[kOpenStreams] -= 1
761770

‎test/http2-headers-timeout.js‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use strict'
2+
3+
const { tspl } = require('@matteo.collina/tspl')
4+
const { test, after } = require('node:test')
5+
const { createSecureServer } = require('node:http2')
6+
const { once } = require('node:events')
7+
8+
const pem = require('@metcoder95/https-pem')
9+
10+
const { Client } = require('..')
11+
12+
// writeH2() armed the stream timer from bodyTimeout for the whole request, so
13+
// headersTimeout had no effect at all over HTTP/2: a server that accepted the
14+
// stream and never sent headers was only cut off after bodyTimeout.
15+
16+
test('headersTimeout is honoured over HTTP/2', async t => {
17+
t = tspl(t, { plan: 2 })
18+
19+
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
20+
21+
// Accept the stream and never respond.
22+
server.on('stream', (stream) => {
23+
stream.on('error', () => {})
24+
})
25+
26+
after(() => server.close())
27+
await once(server.listen(0), 'listening')
28+
29+
const client = new Client(`https://localhost:${server.address().port}`, {
30+
connect: { rejectUnauthorized: false },
31+
allowH2: true,
32+
headersTimeout: 200,
33+
// Much larger, so a failure here means bodyTimeout was used instead.
34+
bodyTimeout: 5000
35+
})
36+
after(() => client.close())
37+
38+
const start = Date.now()
39+
40+
await t.rejects(client.request({ path: '/', method: 'GET' }), {
41+
message: 'HTTP/2: "headers timeout after 200"',
42+
code: 'UND_ERR_HEADERS_TIMEOUT'
43+
})
44+
45+
t.ok(Date.now() - start < 2000, 'must not wait for bodyTimeout')
46+
47+
await t.completed
48+
})
49+
50+
test('bodyTimeout applies once the response headers arrive', async t => {
51+
t = tspl(t, { plan: 2 })
52+
53+
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
54+
55+
// Answer immediately, then stall the body.
56+
server.on('stream', (stream) => {
57+
stream.on('error', () => {})
58+
stream.respond({ ':status': 200 })
59+
})
60+
61+
after(() => server.close())
62+
await once(server.listen(0), 'listening')
63+
64+
const client = new Client(`https://localhost:${server.address().port}`, {
65+
connect: { rejectUnauthorized: false },
66+
allowH2: true,
67+
// Small enough that a request still waiting on headersTimeout would fail
68+
// the wrong way.
69+
headersTimeout: 5000,
70+
bodyTimeout: 200
71+
})
72+
after(() => client.close())
73+
74+
const res = await client.request({ path: '/', method: 'GET' })
75+
t.strictEqual(res.statusCode, 200)
76+
77+
await t.rejects(res.body.text(), {
78+
message: 'HTTP/2: "body timeout after 200"',
79+
code: 'UND_ERR_BODY_TIMEOUT'
80+
})
81+
82+
await t.completed
83+
})

‎test/http2-timeout.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ test('Should handle http2 stream timeout', async t => {
5050
})
5151

5252
await t.rejects(res.body.text(), {
53-
message: 'HTTP/2: "stream timeout after 50"'
53+
message: 'HTTP/2: "body timeout after 50"',
54+
code: 'UND_ERR_BODY_TIMEOUT'
5455
})
5556

5657
await t.completed

0 commit comments

Comments
 (0)