Bug Description
When Client#upgrade() is used over HTTP/2 for a websocket extended CONNECT, the returned promise/callback can hang indefinitely if the peer closes the stream before sending response headers.
Reproducible By
- Start an HTTP/2 server.
- Accept the websocket extended CONNECT stream.
- Close the stream before calling
stream.respond().
- Call
client.upgrade({ path: '/', protocol: 'websocket' }) with allowH2: true.
- Observe that the returned promise never settles.
import { once } from "node:events";
import { createSecureServer } from "node:http2";
import { generate } from "@metcoder95/https-pem";
import { Client } from "undici";
const server = createSecureServer({
...(await generate({ opts: { keySize: 2048 } })),
settings: { enableConnectProtocol: true },
});
server.on("stream", (stream) => {
// No response headers are sent before the stream is closed.
stream.end();
// `stream.close()` or another premature close path appears to trigger the same hang.
});
await once(server.listen(0), "listening");
const client = new Client(`https://localhost:${server.address().port}`, {
allowH2: true,
connect: {
rejectUnauthorized: false,
},
});
await client.upgrade({ path: "/", protocol: "websocket" });
console.log("unreachable: promise stays pending");
Expected Behavior
client.upgrade() should always settle. If the HTTP/2 stream closes before response headers are received, it should reject with a stream/protocol error instead of hanging indefinitely.
Logs & Screenshots
No error is surfaced to the caller in this case. The promise remains pending until the caller provides its own timeout or abort signal.
Environment
macOS 26.4.1
Node v24.14.1
undici v8.1.0
Bug Description
When
Client#upgrade()is used over HTTP/2 for a websocket extended CONNECT, the returned promise/callback can hang indefinitely if the peer closes the stream before sending response headers.Reproducible By
stream.respond().client.upgrade({ path: '/', protocol: 'websocket' })withallowH2: true.Expected Behavior
client.upgrade()should always settle. If the HTTP/2 stream closes before response headers are received, it should reject with a stream/protocol error instead of hanging indefinitely.Logs & Screenshots
No error is surfaced to the caller in this case. The promise remains pending until the caller provides its own timeout or abort signal.
Environment
macOS 26.4.1
Node v24.14.1
undici v8.1.0