skip to content

A gRPC call fails, yet the HTTP :status on its response is 200 — where does the real outcome travel?

level: middleimportance: must knowfreq 74%

answer

  1. HTTP answered a different question
  2. the outcome is not known up front
  3. verdict at the end, not the start
  4. grpc-status lives in trailing metadata
  5. Trailers-Only is the one exception

basics

~20 s

Once the server accepts a gRPC call, the response carries :status 200 whatever happens; the outcome rides in the trailing metadata as grpc-status, with grpc-message beside it. A call that fails immediately uses a Trailers-Only response.

solid answer

~50 s

The HTTP layer only reports whether the *call* was accepted, not how it ended. So a normal response is two header blocks with the messages between them: an initial `HEADERS` frame with `:status 200` and `content-type`, then any `DATA` frames, then a second `HEADERS` frame carrying the **trailer section** — `grpc-status` (a numeric code) and, on failure, `grpc-message`, a percent-encoded description. That final block carries `END_STREAM`, so the trailer section and the end of the response are the same event. The exception is a call that fails before any message is produced: the server may send a single `HEADERS` frame with `END_STREAM` that carries `:status`, `content-type` **and** the status fields together — a **Trailers-Only** response. The rule to hold onto is that the status is trailing metadata, not a response header, except in that one shape.

code

http · 17 lines
http
HEADERS (stream 5, END_HEADERS)
:status = 200
content-type = application/grpc+proto

DATA (stream 5)
<length-prefixed response message>

HEADERS (stream 5, END_STREAM, END_HEADERS)
grpc-status = 0

--- a call that fails on sight: Trailers-Only ---

HEADERS (stream 7, END_STREAM, END_HEADERS)
:status = 200
content-type = application/grpc+proto
grpc-status = 12
grpc-message = method%20not%20found

go deeper

for a junior

Remember the shape: an accepted call answers 200 regardless of outcome, and the real result is grpc-status delivered at the end. Looking for the verdict in the HTTP status line is the classic first mistake.

for a middle

Explain why the design has to be this way — a streaming response is already on the wire before the outcome exists — and name the Trailers-Only exception for a call that fails before producing a message.

for a senior

Make the operational consequence explicit: error rates cannot be read from HTTP status at an intermediary, and every hop that terminates HTTP/2 must forward the trailer section or it erases failures.

for a principal

Weigh the bet: putting the verdict at the end buys honest streaming semantics and costs you legibility at every layer that only understands request-response, which is most of the observability estate you already own.

## The HTTP status answers a different question When a broker files a declaration and the adjudicating service rejects it, two completely different questions have been answered. *Did the request reach a gRPC server that accepted it as a call?* — that is HTTP's question, and the answer is `:status 200`. *How did the call end?* — that is gRPC's question, and HTTP has no field for it, because the outcome is not known when the response headers are written. That last clause is the whole design. A server-streaming call may emit a hundred messages over a minute and then fail. The response headers left the server before the first one. There is no way to put the outcome in them, so gRPC puts it at the **end**, in the trailer section — the HTTP/2 mechanism for fields that are only known once the body is complete. ## The three parts of a response 1. **Initial `HEADERS`.** `:status = 200`, `content-type`, and any metadata the server wants to send up front. Once this is on the wire the call is accepted, and no later failure can change it. 2. **Zero or more `DATA` frames.** The response messages, each length-prefixed. 3. **Trailing `HEADERS`.** The trailer section: `grpc-status`, plus `grpc-message` when the call failed. It carries `END_STREAM`, which is what ends the response. ## The fields the trailer section carries | field | present when | what it holds | |---|---|---| | `grpc-status` | every completed call | a numeric code from the status enum, `0` for success | | `grpc-message` | failures | a short description, percent-encoded so it is header-safe | | `grpc-status-details-bin` | some failures | a structured detail payload, base64-encoded under the `-bin` suffix | What each status *value* means, and how to choose between two adjacent ones, is the business of the deadlines-and-metadata leaf. This leaf owns **where** the outcome travels, and the answer is: at the end, after the messages, in a second header block. ## Trailers-Only, the exception that proves the rule Some calls fail before the server has anything to stream — an unknown method, a rejected credential, a request that fails validation on sight. Sending an empty response and then trailers would be two header blocks for no reason, so the specification allows a **Trailers-Only** response: a single `HEADERS` frame with `END_STREAM` that carries `:status`, `content-type` and the status fields together. This matters far beyond tidiness. Because a Trailers-Only status is delivered as **response headers**, it survives hops that a genuine trailer section does not — which produces the confusing operational picture where a service's fast rejections are perfectly legible while its late failures arrive with no error at all. ## Why `END_STREAM` is the same event as the trailer section Each direction of a call is closed by `END_STREAM`. The client sets it on its last `DATA` frame — for a unary or server-streaming call, the first and only one — which half-closes the request direction while leaving the response direction open. The server sets it on the trailing `HEADERS`. There is no separate "call finished" signal: **the arrival of the trailer section is it**. A response that ends without one is therefore not a successful call with no status; it is a broken call, and the client has to invent an outcome for it. ## What this costs in practice - **Nothing in the HTTP status line tells you whether calls are failing.** An access log at an intermediary showing 100% 200s is consistent with every call failing. Error rate has to come from `grpc-status`, which means the logging hop has to read the trailer section. - **Anything that terminates HTTP/2 in the path must forward trailing fields**, or it silently converts every late failure into a statusless response. - **Downgrading to HTTP/1.1 anywhere in the path is dangerous**, because the trailer section then depends on chunked trailing fields, which many implementations drop. - **A response can be large, correct and still failed.** Messages already delivered on a streaming call are not retracted when the trailer section says the call failed; the receiving application has to decide what to do with the partial result it already consumed. The mental model worth keeping: HTTP framed the conversation, and gRPC wrote the verdict at the bottom of the last page.

  • Why is grpc-message percent-encoded?
    It is a header field value, so it must stay inside the restricted charset a header block allows. Percent-encoding lets an arbitrary human-readable description — spaces, punctuation, non-ASCII — travel safely, and the client decodes it before showing it.
  • A streaming call delivered forty messages and then ended with a failing grpc-status. What has the client already consumed?
    All forty. Messages are delivered as they arrive and are not retracted by the trailer section, so the application has to treat a streamed result as provisional until the status arrives and decide whether a partial result is usable.
  • Can a server change its mind after sending the initial HEADERS?
    Not about the HTTP status — that block has already left. It can still fail the call, but only through the trailer section, which is exactly why the outcome had to be put there rather than in the response headers.

The 200 is the courier's proof that your envelope was delivered and opened. The adjudication is a slip clipped to the last page — so a handler who throws the slip away hands you a perfect delivery receipt and no verdict at all.

saying these in an interview costs you the question

  • Expects the HTTP status to mirror the call's gRPC status
  • Reads an access log of 200s as proof no calls failed
  • Looks for the outcome in the response headers, not trailing metadata
  • Thinks a response with no grpc-status is a successful call
  • Assumes streamed messages are retracted when the status says failure