skip to content

A client sends a request with the HTTP header Expect: 100-continue. What are the possible server behaviours, including HTTP status 417 Expectation Failed, and what must the client do if no response arrives at all?

level: middleimportance: should knowfreq 28%

answer

  1. 100 / final status / 417 / silence
  2. Silence = mutual wait, break it
  3. Client timer then send body anyway
  4. curl's ~1 second
  5. 417 → retry without Expect

basics

~20 s

The server may send 100 Continue, a final status such as 413 rejecting it early, or 417 Expectation Failed. It may also stay silent and just wait for the body, so the client must start a short timer and send the body anyway when it expires.

solid answer

~50 s

Four outcomes: 1. **`100 Continue`** — send the body, then read the final response. 2. **A final status** (`401`, `403`, `413`, `415`, ...) — the request is decided from headers alone; the body is never sent, and the connection is usually closed because unsent or partially-sent body bytes would break framing. 3. **`417 Expectation Failed`** — the expectation itself is refused. The client should retry without the header. In practice `417` is rare; older or simplistic intermediaries emitted it and it is one reason libraries make the behaviour optional. 4. **Silence.** A server that does not implement the expectation may simply block reading the body. Since it is waiting for the client and the client is waiting for it, this is a deadlock unless the client breaks it. That is why the fallback is mandatory: the client waits a bounded time — curl uses one second — and then sends the body regardless. The cost of a non-supporting peer is a fixed delay, not a hang.

code

http · 7 lines
http
POST /ingest HTTP/1.1
Host: api.example.com
Content-Length: 104857600
Expect: 100-continue

HTTP/1.1 417 Expectation Failed
Content-Length: 0

go deeper

for a junior

List the outcomes: go-ahead, early rejection, refusal, or nothing — and know the client eventually sends the body anyway.

for a middle

Explain the mutual-wait deadlock and the mandatory client timer, and that 417 means retry without the header.

for a senior

Cover proxy behaviours, connection handling after early rejection, and recognizing a fixed per-upload delay as an ignored expectation.

for a principal

Treat it as an optimization that must degrade cleanly everywhere, and set client policy (threshold, timeout, on/off) per peer class rather than globally.

## The four outcomes When a request carries `Expect: 100-continue`, the client has stopped writing and is reading. Everything now depends on what comes back. **1. `100 Continue`.** The willing case. It is an interim response: status line, optionally a few headers, empty line, no body. The client writes the body and then reads the real final response. The client must not treat the interim status as the answer. **2. A final response before the body.** The server decided from the headers alone: `401 Unauthorized` (no credentials), `403 Forbidden`, `404 Not Found`, `405 Method Not Allowed`, `413 Content Too Large` (`Content-Length` exceeds the limit), `415 Unsupported Media Type` (wrong `Content-Type`). This is the payoff of the whole mechanism — the rejection cost is a few hundred bytes instead of gigabytes. The server should then either close the connection or be prepared to discard body bytes the client may still send, because leftover bytes in the stream would be misparsed as the start of the next request on a persistent connection. **3. `417 Expectation Failed`.** The specified answer when the server (or an intermediary on the path) will not honour the expectation. The correct client behaviour is to retry the same request without the `Expect` header. Modern origins rarely emit it; it survives mostly in legacy proxies and in some server frameworks that reject unknown expectations strictly. **4. Nothing at all.** The most common real-world case with older or minimal servers: the implementation does not look at `Expect`, so it just blocks trying to read the body. The client is waiting for a status, the server is waiting for bytes — a mutual wait that only ends when something times out. ## Why the timeout fallback is not optional RFC 9110 explicitly tells clients not to wait indefinitely. The required behaviour is: start a timer when the headers have been written; if no response has arrived when it expires, send the body anyway. The server, which was waiting for exactly those bytes, then proceeds normally and the request succeeds — just later. The practical constant is curl's one second. That value shows up constantly in incident reports as an unexplained '~1s' added to every upload against a peer that ignores the expectation. It is a fixed additive latency, not a failure, which is what makes it so easy to overlook and so annoying at volume: a thousand uploads become a thousand extra seconds. ## Intermediaries Proxies complicate every branch. A forwarding proxy is responsible for the expectation toward the origin, and implementations differ: some forward `Expect` and relay the interim `100`, some strip the header and buffer the body themselves, some answer `100 Continue` on their own authority and then discover the origin rejects the request — at which point the client has already uploaded to the proxy. A few legacy proxies drop 1xx responses entirely, which turns case 1 into case 4 from the client's point of view. This is why the same upload can behave differently direct and through a proxy, and why the fallback timer must exist even when you know the origin supports the mechanism. ## Client-side controls Most HTTP clients expose this. curl sends the expectation automatically for bodies over roughly 1 KB and can be told not to by sending an empty header, `-H 'Expect:'`. Java's HttpClient has `expectContinue(boolean)`. Apache HttpClient, Python's requests via urllib3, Go's transport with `ExpectContinueTimeout` — all let you enable, disable or retune the wait. Tuning the timeout down is a legitimate middle ground when you know some peers ignore the header. ## How to reason about it in an interview The crisp framing: this is an optional optimization layered on a protocol that does not require it, so **every branch must degrade to plain HTTP**. Server ignores it → client times out and uploads anyway. Server refuses the expectation → client retries without it. Server rejects the request → the optimization paid off. Nothing about the mechanism is allowed to make a request fail that would otherwise have worked.

  • Why should a server that rejects the request before the body usually close the connection?
    Because the client may already be writing, or may write, body bytes that the server never reads. On a persistent connection those leftover bytes would be parsed as the beginning of the next request and corrupt the framing. Closing, or draining a bounded number of bytes before continuing, keeps the connection state consistent.
  • If a proxy answers 100 Continue itself and the origin then rejects the request, what has been lost?
    The client uploads the whole body to the proxy before the rejection is known, so the bandwidth saving the mechanism was meant to provide is gone on the client-to-proxy leg. The client still gets a correct final status, just after paying the transfer cost — which is why behaviour can differ noticeably between direct and proxied paths.

saying these in an interview costs you the question

  • Believing a client may wait indefinitely for 100 Continue
  • Expecting 417 to be the normal response from servers that do not support the header
  • Assuming the mechanism can cause a request to fail rather than merely be slower
  • Ignoring that proxies may answer, strip or swallow the interim response
  • Not knowing that a fixed ~1s upload delay is a classic symptom of an ignored expectation

context