Repository navigation
Description
Summary
When a client cancels an RPC, the server only learns that the call was cancelled, never why. Today every implementation I checked sends RST_STREAM(CANCEL) regardless of the cause, and the server surfaces CANCELLED.
I'd like to propose a small, opt-in HTTP/2 extension:
- A new HTTP/2 setting that a server advertises to say "I understand gRPC cancellation reason codes".
- A small block of new
RST_STREAMerror codes, one per cancellation reason (deadline expired, hedge/retry attempt superseded, application cancel, ...). - Clients send a reason code only to peers that advertised the setting, and
CANCELto everyone else.
Old peers and proxies never see a new code, so this is backwards compatible by construction. Proxies can adopt it independently, one connection at a time.
Is your feature request related to a problem?
Server-side signals increasingly drive load balancing: ORCA load reports, server-side error rates, and SLO metrics. For these, the reason for a cancellation matters:
| Cause of the client cancel | Is it the server's fault? | What the server sees today |
|---|---|---|
| Client deadline expired while the server was still working | Often yes (slow host) | CANCELLED |
| Hedging: another attempt won, this one was cancelled | No | CANCELLED |
| Retry/hedge committed elsewhere, application cancelled, channel shut down | No | CANCELLED |
Because these are indistinguishable, a server (or a load balancer consuming its reports) has to either:
- count
CANCELLEDas an error, which penalizes healthy hosts for every lost hedge race, or - ignore
CANCELLED, which hides the most important slow-host signal: calls the client gave up on.
The same ambiguity exists on the client side for outlier detection (#12834). There, the client at least knows the reason locally. The server has no way to find out.
Why the server can't infer it today
- The server's own deadline fires late.
grpc-timeoutis relative, so the server's deadline starts when it receives the headers, which is after the client's deadline started. The client'sRST_STREAMusually arrives before the server's own timer fires, so the server recordsCANCELLED, notDEADLINE_EXCEEDED. grpc-previous-rpc-attempts(gRFC A6) isn't enough. It tells the server up front that an attempt is a retry or hedge, but the original attempt (which carries no header) can also lose a hedge race. And the reason for a cancellation is only known at cancel time.- Nothing else can carry the reason. After
RST_STREAM, nothing more can be sent on the stream, and gRPC clients don't send trailers.
Current behaviour (grpc-java)
- The client always resets with
CANCEL:NettyClientHandlerL851, L961. - The server reports any received
RST_STREAMasCANCELLEDwith the description"RST_STREAM received for code N":NettyServerHandlerL606-L620.
Describe the solution you'd like
1. Setting: SETTINGS_GRPC_CANCEL_REASONS (identifier TBD)
- Values:
0= disabled (initial value, as RFC 9113 §5.5 requires for negotiation settings);1= "I accept gRPC cancellation reason codes". - Who sends it: a server sets it to
1on connections where it can interpret the codes. Clients don't need to advertise anything in the client-to-server direction.
2. RST_STREAM error codes (values TBD)
| Name (placeholder) | Meaning |
|---|---|
GRPC_CANCEL_DEADLINE_EXCEEDED |
The client's deadline expired. |
GRPC_CANCEL_ATTEMPT_SUPERSEDED |
A hedge/retry attempt was committed, so this one was abandoned. |
GRPC_CANCEL_APPLICATION |
The application cancelled the call (explicit cancel, or its context was cancelled for reasons other than a deadline). |
GRPC_CANCEL_SHUTDOWN |
The channel is shutting down (e.g. shutdownNow). |
The set should stay small and extensible. A receiver that advertised the setting must treat any code in the block that it doesn't recognize exactly like CANCEL.
3. Client behaviour
- If the peer's most recent
SETTINGShas the setting set to1, reset with the reason code. - Otherwise (including before the peer's first
SETTINGSis processed), reset withCANCEL, exactly as today.
4. Server behaviour
- Cancellation semantics are unchanged: the call is cancelled exactly as for
CANCEL(context cancelled, listeneronCancel, etc.). - Only the reported reason changes. Suggested mapping, with the API exposure left to each implementation:
- deadline:
DEADLINE_EXCEEDED, matching what the server's own deadline timer reports today; - all others:
CANCELLED, with a machine-readable reason visible to stream tracers / interceptors.
- deadline:
- gRPC-based proxies: the server should set the reason as the cancellation cause of the call's context. A gRPC application that proxies calls (cancels its outgoing call when the incoming one is cancelled) can then pass the reason on through its own client. grpc-java already turns an expired context deadline into
DEADLINE_EXCEEDEDon outgoing calls.
5. Registration
- RFC 9113 §5.5 allows extensions to define new settings and error codes.
- The IANA "HTTP/2 Settings" and "HTTP/2 Error Code" registries use Expert Review.
- RFC 9113 §11 folded the former experimental setting range into the normal registry policy, and the error-code registry never had a private range.
- So gRPC would register one setting identifier and a small block of error codes.
Backwards compatibility
- Old servers never advertise the setting, so they never receive a new code.
- Old clients ignore the unknown setting (RFC 9113 §6.5.2: "MUST ignore") and keep sending
CANCEL. - Why not send the codes unconditionally? RFC 9113 §7 lets a receiver treat unknown codes as
INTERNAL_ERROR, and the gRPC HTTP/2 mapping turns that intoINTERNAL. grpc-java servers happen to report any code asCANCELLED, but other implementations and proxies shouldn't have to be relied on. Negotiating removes the risk. - Proxies: HTTP/2 settings apply to a single connection (RFC 9113 §6.5: "SETTINGS frames always apply to a connection, never a single stream"; §5.5: extensions "are effective only within the scope of a single HTTP/2 connection"). A proxy that doesn't know the setting ignores it and doesn't advertise it, so clients behind it keep sending
CANCEL. There's no regression and no flag day. - Direct connections benefit immediately (proxyless/xDS gRPC, client-side load balancing): this is where most service-to-service traffic flows in our deployment.
How proxies can support this over time
Proxies terminate HTTP/2 separately on each side, so "support" means doing the same negotiation on both sides:
- Client side: advertise the setting to clients when the proxy can relay reasons.
- Server side: remember, per upstream connection, whether that server advertised the setting.
- On a client reset: carry the reason code across the proxy. Send it to the server if that connection advertised the setting, otherwise downgrade to
CANCEL.
The proxy needs no per-client state. The decision is made per upstream connection at reset time, and pooled upstream connections shared across clients work fine.
The hard part is step 3: carrying a reset reason from one side of the proxy to the other. HAProxy shows this is achievable, and that it's being built for exactly this gRPC use case.
Precedent: HAProxy forwarding gRPC cancellations (2019 → 2024)
- 2019-07 (HAProxy 1.9): haproxy/haproxy#172 reported that gRPC client deadlines and cancellations didn't reach backends, because HAProxy didn't forward
RST_STREAM. The maintainer traced it to the design issue in haproxy/haproxy#142 ("shutdown vs abort"): the stream layer could express a clean shutdown but had no way to carry an abort and its reason across sides. - 2019–2023: incremental rework. The 2.1 end-of-input flag; a partial improvement backported to 2.2; removal of the
stream_interfacelayer so a mux can store a close status shared with the stream; better error propagation in 2.9. In 2022 the maintainer said the plan requires encoding "the reason for the RST" in the internal layer, so it can be mapped between protocols. - 2024-04 (HAProxy 3.0):
96f8b7ad08added a generic abort reason (32-bit code + source) passed between the two sides. The commit message says forwardingRST_STREAMcodes "is mandatory for gRPC applications". It also says "we can imagine to alter this reason at the applicative level to enrich it".20b156ee15forwards an H2 client'sCANCELto H2 servers, fixing Push Java libraries to Maven central #172. The QUIC mux recordsSTOP_SENDINGcodes the same way.
- Today: the code is carried end to end, then filtered by an allow-list:
h2s_is_forwardable_abort("For now, only CANCEL from the client is forwardable to the server"). It's used when the server-side stream is shut down (L5977, L6066).
Supporting reason codes in HAProxy would now be small: advertise the setting on client connections, record it on server connections (the H2 mux already parses each peer's SETTINGS), and extend the allow-list to forward the new codes when the server advertised them.
Other proxies today
| Proxy | Current behaviour | What it would need |
|---|---|---|
| Envoy | Collapses incoming codes other than REFUSED_STREAM / CONNECT_ERROR / PROTOCOL_ERROR into a generic RemoteReset (errorCodeToResetReason). The router then resets upstream with LocalReset, which is sent as INTERNAL_ERROR (reasonToReset, upstream reset). |
Keep the received code with the downstream reset, pass it through the router, and emit it upstream when that connection advertised the setting. Advertising per side has a precedent: allow_connect for RFC 8441 Extended CONNECT, which is also a SETTINGS-gated extension. |
nginx (grpc_pass) |
ngx_http_grpc_abort_request is a no-op. The upstream connection is closed instead of reset. |
First, forward client cancellations as RST_STREAM at all (the same gap HAProxy closed in #172). Then the same three steps. |
Go net/http2 (Traefik, Caddy, httputil.ReverseProxy) |
Cancelling the request context always sends CANCEL (transport.go L1386). |
A way to attach a reset code to request cancellation, plus advertising the setting. |
| gRPC-based proxies | Cancel the outgoing call with CANCEL. |
Nothing beyond this proposal, if the server exposes the reason as the context's cancellation cause (see §4). |
Each proxy can adopt this on its own schedule, and every hop that hasn't yet falls back to CANCEL. Deployments that go through proxies gain the feature as their proxies upgrade, and nothing breaks before then.
Alternatives considered
- Request header marking hedged attempts: works through proxies today, but only covers hedging. It's set before the outcome is known, and attempt 0 can't be tagged. It's still a useful complement.
- Server-side heuristic (treat
CANCELLEDnear the server's deadline as a deadline miss): depends on latency and the extra time from the relative timeout. It can't tell a late hedge cancellation from a deadline. - Custom extension frame carrying a reason before
RST_STREAM: allows a richer payload, but proxies drop unknown frame types, and it adds ordering questions. Error codes reuse the frame every proxy already handles. - Sending new codes without negotiation: allowed by RFC 9113 §5.5, but risks
INTERNALon old peers (see Backwards compatibility).
Open questions
- Is the gRPC team open to registering a setting and error codes with IANA, or would you prefer another mechanism?
- Which reasons should be in the initial set? Should the server→client direction (e.g. "server overloaded") be in scope later?
- How should each language expose the reason to stream tracers and interceptors?
Activity