Skip to content

fix(h2): requeue request on GOAWAY'd session instead of crashing#5453

Open
staylor wants to merge 1 commit into
nodejs:mainfrom
staylor:fix-h2-goaway-requeue
Open

fix(h2): requeue request on GOAWAY'd session instead of crashing#5453
staylor wants to merge 1 commit into
nodejs:mainfrom
staylor:fix-h2-goaway-requeue

Conversation

@staylor

@staylor staylor commented Jun 26, 2026

Copy link
Copy Markdown

Issue

When a request is dispatched onto an HTTP/2 session that has already received a GOAWAY frame, ClientHttp2Session.request() throws ERR_HTTP2_GOAWAY_SESSION ("New streams cannot be created after receiving a GOAWAY"). In writeH2's requestStream, only ERR_HTTP2_INVALID_SESSION is handled gracefully; GOAWAY falls through to:

session.destroy(wrappedErr)
util.destroy(session[kSocket], wrappedErr)   // <-
abort(wrappedErr)

The util.destroy(socket, wrappedErr) re-emits 'error' on a socket whose 'error' listener can already be detached during teardown, which surfaces as an uncaughtException and crashes the process:

Uncaught exception
  InformationalError: New streams cannot be created after receiving a GOAWAY
  code: UND_ERR_INFO, cause.code: ERR_HTTP2_GOAWAY_SESSION
    at requestStream (lib/dispatcher/client-h2.js)
    at writeH2 → _resume → resume → [dispatch] → Pool.dispatch

Observed in production on a high-volume allowH2: true client talking to servers that routinely send GOAWAY to rotate connections: under load a queued request is dispatched onto a session in the window after the GOAWAY frame is received but before onHttp2SessionGoAway retires the session, and the process crashes. Same family as the recently-hardened h2 teardown crashes (#5440, #4564).

Fix

A GOAWAY'd session is the same situation as ERR_HTTP2_INVALID_SESSION — the peer won't accept new streams — so route it through the existing graceful branch (resetHttp2Session + requeueUnsentRequest). The request is reset and requeued on a fresh connection instead of failing and risking the unhandled socket 'error'.

-      if (err?.code === 'ERR_HTTP2_INVALID_SESSION') {
+      if (err?.code === 'ERR_HTTP2_INVALID_SESSION' || err?.code === 'ERR_HTTP2_GOAWAY_SESSION') {

Test

Added a regression test in test/http2-goaway.js, modeled on the existing http2-invalid-session.js pattern: it stubs http2.connect to return a fake session whose request() throws ERR_HTTP2_GOAWAY_SESSION, then asserts the request is requeued onto a fresh real session and succeeds (connectCalls === 2, 200, 'ok'). It fails on main (the throw escapes via the destroy+abort fallthrough) and passes with this change.

When a request is dispatched onto an HTTP/2 session that has already received
a GOAWAY frame, ClientHttp2Session.request() throws ERR_HTTP2_GOAWAY_SESSION.
requestStream caught it and fell through to session.destroy(err) +
util.destroy(socket, err) + abort(err); the destroy-with-error on a socket
whose 'error' listener is already detached re-emits 'error' and crashes the
process via uncaughtException (observed in production on a high-volume h2
client to servers that routinely GOAWAY to rotate connections).

A GOAWAY'd session is the same situation as ERR_HTTP2_INVALID_SESSION — the
peer won't accept new streams — so route it through the existing graceful
branch (resetHttp2Session + requeueUnsentRequest) so the request retries on a
fresh connection. Adds a regression test (stubs session.request to throw
ERR_HTTP2_GOAWAY_SESSION); it fails on main and passes with the fix.

Signed-off-by: Scott Taylor <scott.c.taylor@mac.com>
Assisted-By: devx/86ca8ae7-2b51-4dd3-8382-dc6116301425
@staylor staylor force-pushed the fix-h2-goaway-requeue branch from fd48680 to 1b329b2 Compare June 26, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant