Skip to content

fix(h2): replay requests whose HEADERS raced a GOAWAY - #6065

Merged
mcollina merged 2 commits into
nodejs:mainfrom
mohamed-rdr:bugfix/h2-graceful-goaway-requeue
Oct 24, 2026
Merged

mcollina merged 2 commits into
nodejs:mainfrom
mohamed-rdr:bugfix/h2-graceful-goaway-requeue

Conversation

@mohamed-rdr

Copy link
Copy Markdown
Contributor

This relates to...

Refs #6064

Rationale

When a server starts a graceful shutdown with GOAWAY(NO_ERROR, 2^31-1), a request dispatched just before the client processes the GOAWAY can have its stream opened but its HEADERS not flushed yet. nghttp2 then refuses to send it (no new streams after a received GOAWAY) and emits frameError (type HEADERS) on the stream. Because lastStreamID covers the stream, onHttp2SessionGoAway leaves the request attached to the session, and onFrameError rejects it with HTTP/2: "frameError" received - type 1, code 2, even though the server never saw it.

Streams that were sent but fall above the final GOAWAY's lastStreamID (hyper/Go send GOAWAY(2^31-1) + PING, then GOAWAY(last)) are already refused by nghttp2 with REFUSED_STREAM and retried by the existing refused-stream path.

The stalls in the issue's reproduction come from the node:http2 server: nghttp2 ignores every new stream once it has sent a GOAWAY, whatever lastStreamID it advertised, and never follows up with a final GOAWAY. The client receives no signal for those streams, so they can only time out.

Changes

Features

N/A

Bug Fixes

  • client-h2: on a frameError for a HEADERS frame after the session received GOAWAY, requeue the request on a new connection if no response started and the body is replayable. Uses the existing GOAWAY replay budget.
  • Extract the requeue step shared with retryRefusedStream into requeueStreamRequest.
  • Add test/http2-graceful-goaway.js: 400 requests with 4 in flight against a server that retires each session every 8 streams, with GOAWAY(0) and with a graceful GOAWAY(2^31-1) + PING + GOAWAY(last). The graceful case failed with frameError before this change.

Breaking Changes and Deprecations

N/A

Status

🤖 Generated with Claude Code

A stream opened before the client processes a GOAWAY but flushed after it is
refused by nghttp2 and surfaces as a frameError on the HEADERS frame. When the
GOAWAY's lastStreamID covers the stream (2^31-1, the graceful shutdown notice
hyper, Go and nghttp2 send), the request stays attached to the session and the
frameError rejected it, although the peer never saw it.

Requeue such requests when the body is replayable, within the GOAWAY replay
budget.

Refs: nodejs#6064

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Mohamed Elsharnouby <mohamed@radar.com>
@codecov-commenter

codecov-commenter commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.61%. Comparing base (935aa75) to head (e9fd9d3).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6065      +/-   ##
==========================================
+ Coverage   91.56%   91.61%   +0.04%     
==========================================
  Files         113      113              
  Lines       43938    44044     +106     
==========================================
+ Hits        40234    40349     +115     
+ Misses       3704     3695       -9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Signed-off-by: Matteo Collina <hello@matteocollina.com>

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina
mcollina requested a review from metcoder95 October 23, 2026 18:27
@mcollina
mcollina merged commit 046bb4c into nodejs:main Oct 24, 2026
38 checks passed
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.

3 participants