fix(request): don't fail requests on HTTP/2 GOAWAY or refused streams - #3302
petersutter wants to merge 3 commits into
Conversation
Record GOAWAY and RST_STREAM state per stream and wrap transport errors in a StreamError carrying the termination descriptor, so logs can tell refused, GOAWAY-cut and truncated streams apart. Messages are unchanged. Signed-off-by: Peter Sutter <peter.sutter@sap.com>
After a graceful GOAWAY, sessions still serving accepted streams are skipped for new requests instead of destroyed, so a sibling request no longer truncates their responses. Draining sessions leave the pool once they close and no longer count as free capacity. Signed-off-by: Peter Sutter <peter.sutter@sap.com>
request() retries a request of any method when the server refused its stream or cut it with a GOAWAY, up to maxRetries (default 2), within a single requestTimeout budget. A stream that failed with its connection before getting an id is not retried. stream() and fetch() never retry. Signed-off-by: Peter Sutter <peter.sutter@sap.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (7)
Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe request client now classifies HTTP/2 stream termination, retries eligible requests within configured limits, and shares a timeout budget across attempts. The session pool records GOAWAY frames and retains draining sessions while their accepted streams finish. ChangesHTTP/2 request retries and session draining
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant Client
participant SessionPool
participant HTTP2Peer
Client->>SessionPool: Request a stream
SessionPool->>HTTP2Peer: Send request
HTTP2Peer-->>SessionPool: Refuse stream or send GOAWAY
SessionPool-->>Client: Return StreamError with termination metadata
Client->>Client: Check retry eligibility and timeout budget
Client->>SessionPool: Request another stream when eligible
Merge Risk: ⚪ Minimal · up to The identified GOAWAY race cannot occur on this path, and retries do not consume a one-shot request body. The change is mergeable after normal checks. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to Automatic retries now include write requests, while active connections can remain open during draining. Retry limits and checks that the server did not process a stream reduce the risk, but the behavior warrants design review. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
How to categorize this PR?
/area robustness
/kind bug
What this PR does / why we need it:
Occasionally, one request in a burst of concurrent requests to the Kubernetes API server fails with
ERR_STREAM_PREMATURE_CLOSEor a truncated body, and the user gets a500. The client can't tell why, because it ignores the stream'srstCodeand GOAWAY frames.Improvements:
StreamErrorthat keeps the originalcode, message andcause, and carrieserr.termination, which says whether the server processed the request. Each failure logs oneRequest <method> <path> [<x-request-id>] failed: ...; termination=...line. Messages shown to users are unchanged.request()retries a request of any HTTP method when the server refused its stream or cut it with a GOAWAY, up tomaxRetriestimes (default 2). Go's HTTP/2 client does the same, and kube-apiserver's--goaway-chancerelies on it. Connection failures are not retried.stream()andfetch()never retry, so watches keep the reflector's own retry loop.Not fixed here: a Node.js bug. With this PR deployed on a development landscape, the remaining
ERR_STREAM_PREMATURE_CLOSEfailures turned out not to come from the API server. They were logged as processed (neverProcessed: false,responseReceived: true, no GOAWAY), so they are correctly not retried. They are caused by a regression in Node.js v24.20.0 and later: when an HTTP/2 stream closes normally before the client starts reading its fully buffered body, async iteration reports a premature close (nodejs/node#65677). The fix, nodejs/node#65689, is not released yet, and the dashboard has run Node 24.21.0 since #3210. This PR does not work around the regression. The improvements above address failure modes that are independent of it.Which issue(s) this PR fixes:
Fixes #
Special notes for your reviewer:
Easiest to review commit by commit.
Related PR: #3301
Release note:
Summary by CodeRabbit