Bug Description
Graceful http2 GOAWAY is handled as hard connection failure
Reproducible By
import { once } from "node:events";
import { createSecureServer } from "node:http2";
import pem from "@metcoder95/https-pem";
import { Client } from "undici";
const server = createSecureServer(
await pem.generate({ opts: { keySize: 2048 } }),
);
const seen = [];
let session;
server.on("session", (s) => {
session = s;
});
server.on("stream", (stream, headers) => {
seen.push(headers[":path"]);
stream.respond({ ":status": 200 });
setTimeout(() => {
if (!stream.closed) {
stream.end(`ok:${headers[":path"]}`);
}
}, 200);
if (seen.length === 3) {
setTimeout(() => {
session.goaway();
}, 50);
}
});
await once(server.listen(0), "listening");
const client = new Client(`https://localhost:${server.address().port}`, {
pipelining: 10,
connect: { rejectUnauthorized: false },
});
const results = await Promise.allSettled(
[1, 2, 3].map((i) =>
client
.request({ path: `/${i}`, method: "GET" })
.then(async (res) => res.body.text()),
),
);
console.log("server saw", seen);
console.log(
results.map((r) => {
return r.status === "fulfilled"
? { status: r.status, value: r.value }
: { status: r.status, code: r.reason.code, message: r.reason.message };
}),
);
await client.close();
await new Promise((resolve) => server.close(resolve));
Observe that the server receives all 3 requests before GOAWAY, but all 3 client requests reject.
Expected Behavior
A graceful code-0 GOAWAY should stop the client from creating new streams on that session, but streams already accepted by the server should still be allowed to finish when they are within lastStreamID.
For the repro above, I would expect the 3 in-flight requests to resolve successfully because the server had already accepted them before calling session.goaway().
Logs & Screenshots
Observed output on this checkout:
server saw [ '/1', '/2', '/3' ]
[
{
status: 'rejected',
code: 'UND_ERR_SOCKET',
message: 'HTTP/2: "GOAWAY" frame received with code 0'
},
{
status: 'rejected',
code: 'UND_ERR_SOCKET',
message: 'HTTP/2: "GOAWAY" frame received with code 0'
},
{
status: 'rejected',
code: 'UND_ERR_SOCKET',
message: 'HTTP/2: "GOAWAY" frame received with code 0'
}
]
Environment
macOS 26.4.1
Node v24.15.0
undici v8.1.0
Bug Description
Graceful http2 GOAWAY is handled as hard connection failure
Reproducible By
Observe that the server receives all 3 requests before
GOAWAY, but all 3 client requests reject.Expected Behavior
A graceful code-0
GOAWAYshould stop the client from creating new streams on that session, but streams already accepted by the server should still be allowed to finish when they are withinlastStreamID.For the repro above, I would expect the 3 in-flight requests to resolve successfully because the server had already accepted them before calling
session.goaway().Logs & Screenshots
Observed output on this checkout:
server saw [ '/1', '/2', '/3' ] [ { status: 'rejected', code: 'UND_ERR_SOCKET', message: 'HTTP/2: "GOAWAY" frame received with code 0' }, { status: 'rejected', code: 'UND_ERR_SOCKET', message: 'HTTP/2: "GOAWAY" frame received with code 0' }, { status: 'rejected', code: 'UND_ERR_SOCKET', message: 'HTTP/2: "GOAWAY" frame received with code 0' } ]Environment
macOS 26.4.1
Node v24.15.0
undici v8.1.0