Bug Description
HTTP/2 stream timeouts decrement session open-stream count twice
Reproducible By
- Start an HTTP/2 TLS server that sends response headers immediately but delays the response body longer than the client's
bodyTimeout.
- Create an Undici
Client and a small bodyTimeout such as 50.
- Send a request and read
res.body.text() so the request hits the HTTP/2 stream timeout path.
- Inspect
client[kHTTP2Session] after the timeout and read the session symbol whose description is open streams.
- Observe that the counter is
-1 after a single timed-out request.
Minimal repro
import { createSecureServer } from 'node:http2'
import { once } from 'node:events'
import pem from '@metcoder95/https-pem'
import { Client } from 'undici'
import symbols from 'undici/lib/core/symbols.js'
const { kHTTP2Session } = symbols
const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
server.on('stream', (stream) => {
stream.respond({ ':status': 200, 'content-type': 'text/plain' })
setTimeout(() => stream.end('late body'), 300)
})
await once(server.listen(0), 'listening')
const client = new Client(`https://localhost:${server.address().port}`, {
connect: { rejectUnauthorized: false },
bodyTimeout: 50
})
const res = await client.request({ path: '/', method: 'GET' })
try {
await res.body.text()
} catch (err) {
console.log('request error:', err.message)
}
const session = client[kHTTP2Session]
const openStreams = Object.getOwnPropertySymbols(session)
.find((sym) => sym.description === 'open streams')
console.log('open streams after timeout:', session[openStreams])
await client.close()
await new Promise((resolve) => server.close(resolve))
Expected Behavior
A timed-out HTTP/2 request should decrement session[kOpenStreams] exactly once, leaving the counter at 0 after the stream is closed. The timeout path should not underflow the counter or leave future stream accounting inconsistent.
Logs & Screenshots
Output for repro
request error: HTTP/2: "stream timeout after 50"
open streams after timeout: -1
Environment
macOS 26.4.1
Node v24.14.1
undici v8.1.0
Bug Description
HTTP/2 stream timeouts decrement session open-stream count twice
Reproducible By
bodyTimeout.Clientand a smallbodyTimeoutsuch as50.res.body.text()so the request hits the HTTP/2 stream timeout path.client[kHTTP2Session]after the timeout and read the session symbol whose description isopen streams.-1after a single timed-out request.Minimal repro
Expected Behavior
A timed-out HTTP/2 request should decrement
session[kOpenStreams]exactly once, leaving the counter at0after the stream is closed. The timeout path should not underflow the counter or leave future stream accounting inconsistent.Logs & Screenshots
Output for repro
Environment
macOS 26.4.1
Node v24.14.1
undici v8.1.0