Skip to content

HTTP/2: retry streams reset with REFUSED_STREAM or rejected by GOAWAY (RFC 9113 §8.7)#1298

Merged
quinnj merged 4 commits into
JuliaWeb:masterfrom
mathieu17g:fix/h2-refused-stream-retry
Jun 15, 2026
Merged

HTTP/2: retry streams reset with REFUSED_STREAM or rejected by GOAWAY (RFC 9113 §8.7)#1298
quinnj merged 4 commits into
JuliaWeb:masterfrom
mathieu17g:fix/h2-refused-stream-retry

Conversation

@mathieu17g

Copy link
Copy Markdown
Contributor

Fixes #1294.

  • New H2StreamResetError carries the RFC 9113 §7 error code (the RSTStreamFrame handler previously discarded it); showerror names the code, e.g. HTTP/2 stream reset by peer: REFUSED_STREAM.
  • _retryable_request_error now classifies REFUSED_STREAM resets and H2GoAwayError as retryable, and _h2_guaranteed_unprocessed lifts the idempotent-method gate for them per §8.7 (the replayable-body gate still applies).
  • Retries land on a fresh connection via the existing _drop_h2_conn!-on-error behaviour.
  • Tests: a scripted h2 server refuses the first request (RST_STREAM(REFUSED_STREAM) / GOAWAY(last_stream_id=0)) then answers 200 — red before the fix (single attempt surfaced as an error), green after (two attempts, fresh connection).

Note: stream resets now raise H2StreamResetError (an HTTPError) instead of a generic ProtocolError — same pattern as the existing H2GoAwayError.

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.09%. Comparing base (426b15b) to head (25171e6).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1298      +/-   ##
==========================================
+ Coverage   86.88%   87.09%   +0.20%     
==========================================
  Files          28       28              
  Lines       10898    11033     +135     
==========================================
+ Hits         9469     9609     +140     
+ Misses       1429     1424       -5     

☔ 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.

Comment thread test/http2_refused_stream_retry_tests.jl
mathieu17g and others added 3 commits June 12, 2026 11:13
…y GOAWAY are never retried

Scripted-server tests (patterned on http2_client_tests.jl): the server refuses
the first request - RST_STREAM(REFUSED_STREAM) or GOAWAY(last_stream_id=0) -
then answers 200 to any later attempt, on the same or a new connection. With
retry=true the client currently surfaces ProtocolError / H2GoAwayError after a
single attempt; per RFC 9113 section 8.7 both cases are guaranteed unprocessed
and safe to retry. Red until the retry fix lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…C 9113 8.7)

A stream reset with REFUSED_STREAM, or rejected by GOAWAY
(stream_id > last_stream_id), is guaranteed unprocessed by the server and is
safe to retry even for non-idempotent requests (RFC 9113 section 8.7). The
client previously surfaced these as ProtocolError("HTTP/2 stream reset by
peer") / H2GoAwayError after a single attempt, with the RST_STREAM error code
discarded.

- New H2StreamResetError carries the RFC 9113 section 7 error code; the
  RSTStreamFrame handler stores it instead of a generic ProtocolError, and
  showerror names the code (REFUSED_STREAM, CANCEL, ENHANCE_YOUR_CALM, ...).
- _retryable_request_error classifies REFUSED_STREAM resets and H2GoAwayError
  as retryable; _h2_guaranteed_unprocessed lifts the idempotent-method gate
  for them (the replayable-body gate still applies).
- The existing _drop_h2_conn! on error already makes the retry use a fresh
  connection.

Greens the tests added in the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The new test file was not listed in test/runtests.jl, so CI never executed it
(codecov flagged the fix lines as uncovered). Also cover the codes without an
unprocessed guarantee: a CANCEL reset surfaces H2StreamResetError on first
occurrence without retry, and showerror names the RFC 9113 section 7 codes
(REFUSED_STREAM, ENHANCE_YOUR_CALM, hex fallback for unknown codes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@quinnj quinnj force-pushed the fix/h2-refused-stream-retry branch from e81bd82 to 1f1016c Compare June 12, 2026 17:13
…scenarios

codecov flagged _h2_guaranteed_unprocessed as uncovered: every existing test
used GET, for which the idempotent-method gate short-circuits before the
section 8.7 check is reached. Add POST scenarios: a refused POST and a POST
rejected by GOAWAY are retried (in-memory body, replayable), while a cancelled
POST surfaces without retry. The scripted server now skips request DATA frames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mathieu17g

Copy link
Copy Markdown
Contributor Author

The remaining codecov gap was the §8.7 non-idempotent path (_h2_guaranteed_unprocessed), which GET requests short-circuit; 25171e6 covers it with POST scenarios (refused → retried, GOAWAY-rejected → retried, cancelled → surfaced without retry).

@mathieu17g

Copy link
Copy Markdown
Contributor Author

Should be ready for merge

@quinnj quinnj merged commit 427b525 into JuliaWeb:master Jun 15, 2026
8 checks passed
@mathieu17g mathieu17g deleted the fix/h2-refused-stream-retry branch June 16, 2026 15:47
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.

HTTP/2: streams reset with REFUSED_STREAM or rejected by GOAWAY are never retried (RFC 9113 §8.7)

2 participants