Skip to content

Graceful http2 GOAWAY is handled as hard connection failure #5089

@trivikr

Description

@trivikr

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions