skip to content

An HTTP/1.1 response starts with a line like 'HTTP/1.1 404 Not Found'. What happened to that trailing reason phrase in HTTP/2 and HTTP/3, and what breaks in software that depends on it?

level: seniorimportance: nice to knowfreq 28%

answer

  1. h1 status line: version, code, phrase
  2. phrase = advisory, may be empty
  3. h2/h3: only the :status pseudo-header
  4. statusText empty or synthesized locally
  5. put detail in the body, never the phrase

basics

~20 s

HTTP/2 and HTTP/3 dropped reason phrases entirely: a response carries only the three-digit :status pseudo-header. Anything that read, logged, asserted on, or smuggled information through the phrase gets nothing — clients synthesize a phrase from a lookup table or leave it empty.

solid answer

~50 s

In HTTP/1.1 the status line is `version SP code SP reason-phrase`, and the phrase was always advisory — RFC 9112 says clients should ignore it and servers may send it empty. HTTP/2 (RFC 9113) and HTTP/3 (RFC 9114) removed it: the response head is a header block whose `:status` pseudo-header holds three digits and nothing else. What breaks is anything that treated the phrase as data. Servers that stuffed an error message into it — `HTTP/1.1 400 Missing customer_id` — silently lose it the moment traffic upgrades to h2 or passes a translating proxy. Tests asserting `statusText === 'Not Found'` fail, because `fetch` over h2 returns an empty `statusText`. Log pipelines that parse phrases see blanks or gateway-synthesized text. The rule that follows: status codes are the machine contract, the response body carries human- and machine-readable detail. Never the reason phrase.

code

http · 6 lines
http
HTTP/1.1 404 Not Found
content-type: application/problem+json

--- HTTP/2 equivalent (decoded header block) ---
:status: 404
content-type: application/problem+json

go deeper

for a junior

Know the h1 status line has a phrase, that it is advisory, and that HTTP/2 sends only the numeric status.

for a middle

Explain the :status pseudo-header and why statusText comes back empty or synthesized on modern connections.

for a senior

Diagnose the real failure mode — error text lost between an h1 dev server and an h2 edge — and move detail into a structured body.

for a principal

Frame it as contract hygiene: the machine contract is code plus structured body; free-text protocol fields are a liability that also carried the h1 CRLF-injection risk.

## Where the phrase came from HTTP/1.x is a text protocol, and its response begins with a status line: the protocol version, a space, the three-digit code, a space, and a free-text **reason phrase** — `HTTP/1.1 503 Service Unavailable`. The phrase existed so a human reading a telnet session could see what happened. It was never part of the machine contract: the specification states that the phrase is advisory, that recipients SHOULD ignore it, and that a server MAY send an empty phrase. Nothing requires the phrase to match the registered text for the code, and nothing requires it to be present. ## What HTTP/2 and HTTP/3 did HTTP/2 replaced the text head with a binary, HPACK-compressed header block, and HTTP/3 did the same with QPACK. Both express the response head as header fields only, with control information carried in **pseudo-headers** that begin with a colon. For responses there is exactly one: `:status`, whose value is the three-digit code as a string. There is no version token and no reason phrase in the wire format — they were deliberately removed as redundant bytes that carried no protocol meaning. Header field names are also lowercase, and connection-specific fields (`Connection`, `Transfer-Encoding`, `Keep-Alive`, `Upgrade`) are forbidden. A client stack still has to present *something* to application code that expects `statusText`. Implementations diverge: browsers over h2 return an empty string for `Response.statusText`; some libraries look the code up in a static table and synthesize the canonical phrase; a proxy translating h2 to h1 for a legacy backend has to invent the phrase from that same table. In every case the value is manufactured locally, not received from the origin. ## What actually breaks - **Error detail smuggled into the phrase.** Frameworks let you do it — Java's `HttpServletResponse.sendError(400, "missing customer_id")` historically set the phrase, and hand-rolled servers write it directly. Over h2, or through any translating hop, the message evaporates. Support then debugs a bare `400` with no context. - **Assertions and log parsing.** Tests written as `expect(res.statusText).toBe('Not Found')` pass on an h1 dev server and fail behind an h2 edge. Log pipelines with a regex over the status line get empty or synthesized text, which quietly changes dashboards. - **Client branching on phrase text.** Any `if (statusText === ...)` is broken twice over: the phrase was never normative even in h1, and it is absent in h2/h3. - **Header injection defences.** CRLF injection into a reason phrase was a real response-splitting vector in h1; it simply has no analogue in h2/h3, which is one of the reasons the field was dropped. ## The design lesson The status code is the machine-readable contract — three digits, a class, registered semantics. Everything else belongs in the **response body**, in a structured format the client can parse: an error object with a stable machine code, a human message, and any field-level detail. `application/problem+json` is the standard shape for that. Headers are the second-best place for machine-readable signals (`Retry-After`, `WWW-Authenticate`), and they survive all HTTP versions. A useful diagnostic habit follows from this: when a bug report says "the error message disappeared in production", check whether production terminates HTTP/2 at the edge while development runs HTTP/1.1. The same code path, the same status, a phrase that exists in one environment and not the other, is a classic version-dependent ghost.

  • Where should a server put a machine-readable error identifier instead?
    In the response body, as a structured document with a stable error code field plus a human-readable message, for example application/problem+json. Bodies survive every HTTP version and every intermediary, can be versioned, and can carry field-level detail that neither a status code nor a header can express.
  • If reason phrases were always advisory, why did removing them matter at all?
    Because plenty of real software ignored the advisory status and treated the phrase as data — error messages, test assertions, log regexes. Removal turned a latent contract violation into a visible failure the first time traffic moved to HTTP/2, which is why it surfaces as an environment-specific mystery rather than a clean break.

saying these in an interview costs you the question

  • Believing the reason phrase is normative and must match the registered text
  • Putting error details in the reason phrase because 'the status code is too coarse'
  • Asserting on statusText in tests and assuming it is stable across environments
  • Claiming HTTP/2 sends the phrase but compresses it away
  • Thinking the version token is still on the wire in HTTP/2

context