Internet Protocols · HTTP

Status Codes, Interim Responses, and When a Response Has No Body

How the three-digit status code steers client behaviour, why 1xx responses break naive parsers, and which statuses forbid or require a body.

The status code is the only part of a response that every client must understand. Field names, media types, and body encodings are all negotiable in the sense that an unknown one can be ignored; a status code that a client does not recognise has to be interpreted from its first digit alone. That design is why the code space is partitioned by class rather than enumerated: a client written against the codes of 1997 still does something defensible with a code defined in 2024.

What the first digit licenses

The five classes are defined by the sender’s intent, not by the client’s action. 2xx means the request was received, understood, and accepted. 3xx means the client must take further action to reach the resource. 4xx means the request as sent is the problem, and the same request will usually fail again unless the condition that caused it has changed. 5xx means the server failed while the request may have been fine.

The last distinction carries an operational consequence that is easy to miss: some 4xx codes are defined as heuristically cacheable, because the failure is a property of the request and will recur for an identical one. 404, 405, 410, and 414 are on that list; 400 and 403 are not. 501 is the only 5xx code on it, because it reports a permanent limitation rather than a transient failure. The other 5xx codes are not heuristically cacheable, and a cache that stores a 500 for an hour turns a momentary fault into an outage. RFC 9111 permits a cache to serve a stale response when it is disconnected from the origin, unless a directive such as must-revalidate prohibits it.

Codes are extensible and clients must treat an unrecognised code as its class. A client that receives 429 from a server it has never heard of should apply 4xx semantics, and one that receives 418 should not crash. Servers cannot simply add codes of their own: values outside the range 100 to 599 are invalid, and a client that receives one should process the response as a 5xx. A code intended for public use has to be defined in a class and registered, which requires review rather than a unilateral decision.

The response that arrives before the response

A 1xx response is not the end of an exchange. It is an interim message: the server sends the status line and field section, then continues with the real response on the same connection. A client parsing a response must therefore loop, discarding 1xx messages, until it sees a status code of 200 or higher. A parser that reads one status line and starts reading a body will treat the following final response as body bytes and corrupt the connection.

100 Continue exists to let a client avoid sending a large body the server will reject. The client sends the request head with Expect: 100-continue and waits; the server either sends 100 Continue and the client transmits the body, or sends a final status such as 417 or 413 and the client sends nothing. The mechanism only pays off when the round trip is cheaper than the upload, which is true for large bodies and false for small ones. A client that waits indefinitely for a 100 is broken, because servers are permitted to ignore the expectation and a final response may arrive directly; the client must have a timeout and then send the body anyway.

101 Switching Protocols is the other common interim code, and it is not really interim: it terminates HTTP on the connection and hands the byte stream to another protocol, conventionally named in the Upgrade field. Once the 101 is sent, nothing in the remaining bytes is HTTP, so a client that keeps parsing frames will misread the tunnel payload as messages.

103 Early Hints is the third, and it is the one that changes ordering assumptions. It carries link headers that a client may act on before the final response exists, which is useful for starting preloads during a slow server render. The client must still wait for the final response and must not treat the early hints as complete.

Body presence is a rule, not an inference

Whether a response has a body is determined by the status code and the method, and only then by the framing fields. RFC 9110 states it directly: responses to HEAD never have a body, and neither do 1xx, 204, and 304 responses. For those, the message ends at the empty line after the field section, regardless of what Content-Length or Transfer-Encoding says. The fields are not merely ignorable there; a server must not send Content-Length in a 1xx or 204 response at all, and a recipient that sees one should treat the message as faulty rather than as an instruction to read bytes. The 304 case is the exception that proves the rule, since it legitimately repeats the length of the representation it is validating.

This rule is where the interaction with message delimitation and request smuggling becomes sharp. A proxy that reads a 304’s Content-Length as a message length will consume bytes belonging to the next response on that connection, because the field describes a representation the client already holds rather than the bytes about to arrive. The specification’s ordering is deliberate: status and method first, framing fields second, and never the other way around.

For 2xx responses to CONNECT, the situation inverts: the response has no content, and the connection becomes a tunnel carrying whatever the client and server agree on. A proxy that tries to read a body after a successful CONNECT will block forever or consume tunnel data.

Redirects and the method they preserve

The 3xx class is where status codes most visibly change client behaviour, because the choice between codes determines whether a request body survives the redirect.

301 and 302 are the historical pair. Their early definitions left it ambiguous whether a redirected request kept its method, and browsers converged on rewriting POST to GET for both; RFC 9110 now documents that behaviour as the expected one for 301 and 302: a user agent may change the method to GET. A server that needs the method preserved must use 307 or 308, which forbid the change. The distinction between 301 and 308 on one side and 302 and 307 on the other is permanence: 301 and 308 are permanent and heuristically cacheable, while 302 and 307 are temporary and not cacheable by default. A permanent redirect that is later reversed can therefore be remembered by a client or a cache long after the change, since a heuristically assigned lifetime is the cache’s choice and not the origin’s.

303 See Other means the response to the request can be found at another URI and must be retrieved with GET, regardless of the original method. It is the correct answer to a successful POST whose result is a resource, since it converts a non-idempotent submission into a safe retrieval.

304 Not Modified is the fourth member and the only one that is not a redirect at all. It belongs to the conditional request mechanism covered in conditional requests and entity validators: the client sent a validator, the server confirmed the stored copy is current, and the response carries no body. Its field section is restricted: it should carry the fields that would have changed, plus caching metadata, and omit representation metadata the client already has.

A redirect chain also has a cost that the codes do not express. Each hop is a round trip, and on a fresh connection each hop may also be a DNS lookup and a TLS handshake. Codes do not limit chain length; implementations impose their own limits, and a loop of permanent redirects that a client follows without a hop cap is an infinite request.

Temporary conditions and the Retry-After field

Three codes describe a temporary condition rather than a verdict on the request.

429 Too Many Requests, from RFC 6585, indicates rate limiting and may carry Retry-After. 503 Service Unavailable may carry the same field when the server expects to recover. 408 Request Timeout is the third member of the group and is different: it reports that the server did not receive a complete request message within the time it was prepared to wait, and it has no retry timing of its own. For the two that do carry Retry-After, the value is either a number of seconds or an HTTP-date. The difference matters when the two hosts’ clocks disagree: a delta-seconds value is relative to the moment of receipt and is therefore unaffected by skew between the hosts, while a date is absolute and depends on the recipient’s clock agreeing with the sender’s. The specification allows either, so a client must implement both.

Retry-After on a 3xx response has a separate meaning: it tells the client how long to wait before requesting the target. Sending it on a 301 that points to a not-yet-deployed resource produces a client that waits and then fails.

The 5xx class also contains codes that describe a permanent limitation rather than a failure. 501 Not Implemented means the method is not supported by the server, which will not change on retry and should be answered with Allow listing what is. 505 HTTP Version Not Supported is likewise permanent. Distinguishing permanent from transient 5xx matters for any client that retries: 500, 502, 503, and 504 are worth retrying with backoff, while 501 and 505 are not.

References

  1. RFC 9110: HTTP Semantics — IETF
  2. RFC 9111: HTTP Caching — IETF
  3. RFC 6585: Additional HTTP Status Codes — IETF