417 Expectation Failed
Client error response defined by RFC 9110 §15.5.18.
Last updated July 27, 2026
Common causes at a glance
- An HTTP/1.0 hop or legacy proxy that cannot honour Expect: 100-continue
- A load balancer or WAF configured to reject any Expect header outright
- A client library adding the expectation automatically for a large body
- A non-standard expectation token that no server has ever implemented
Reproduce it
curl -i -X POST -H 'Expect: 200-ok' --data-binary @body.json https://api.example.com/v1/importWhat 417 Expectation Failed means
The Expect mechanism lets a client state a requirement that
must hold before the request proceeds. A server that cannot satisfy it must fail the
request rather than guess, and 417 is that failure. The design anticipated a family of
expectations; only 100-continue was ever standardised, so the mechanism is
one feature with an extensible syntax nobody used.
The practical consequence is that 417 means “this hop is old or strict”, not “your request is wrong”. The body was never read, so nothing needs undoing, and retrying without the header normally succeeds immediately.
What the spec says
RFC 9110 §15.5.18 defines the code, and
§10.1.1 defines Expect. Three rules matter. An
origin server receiving an expectation it cannot meet must respond 417 unless the
request can be satisfied another way. A proxy that receives 100-continue
and knows the next hop is HTTP/1.0 must answer 417 itself rather than forward it,
because the older version has no interim responses. And a client must not wait
indefinitely for
100 Continue: after a short timeout it should send the body
anyway, since an intermediary may have dropped the expectation. That last rule is why
mishandled expectations more often show up as latency than as 417.
What actually causes it
curl adds Expect: 100-continue on its own once a
PUT or POST body passes roughly a kilobyte, so a 417 often
appears with no code change on your side, just a payload that grew. The peer refusing it
is typically an older appliance, an HTTP/1.0-speaking hop, or a security proxy whose
policy is to reject headers it does not model. Python’s requests and
browser fetch never send the expectation, which is why a request that fails
from a shell script works from application code, and why “curl reproduces it but the app
does not” is such a common report. A nonsense token such as Expect: 200-ok
reliably forces a 417 for testing.
How to debug it
Confirm the header is the variable by removing it. An empty value tells curl to omit it entirely:
curl -i -X POST --data-binary @body.json -H 'Expect:' https://api.example.com/v1/import
If that succeeds and the same request with -H 'Expect: 100-continue'
returns 417, you have your answer. Next, work out which hop objects: send the request
straight to the origin, bypassing the CDN or load balancer, and compare. A 417 at the
edge and a 100 at the origin means the intermediary is the strict one, and that is where
the configuration change belongs. For clients you control, suppressing the expectation
is almost always right; the bandwidth it saves matters only when rejections are common
and bodies are large.
Working examples
curl
# Force a 417 with an expectation nobody implements.
curl -i -X POST -H 'Expect: 200-ok' \
--data-binary @body.json \
https://api.example.com/v1/import
# curl adds Expect: 100-continue for bodies over ~1 KB. Suppress it:
curl -i -X POST -H 'Expect:' \
-H 'Content-Type: application/json' \
--data-binary @body.json \
https://api.example.com/v1/import
Python (requests)
import requests
url = "https://api.example.com/v1/import"
body = open("body.json", "rb").read()
# requests never adds Expect on its own, so opt in to reproduce a shell failure.
with_expect = requests.post(
url,
data=body,
headers={"Expect": "100-continue", "Content-Type": "application/json"},
timeout=60,
)
print("with expect:", with_expect.status_code)
without = requests.post(
url, data=body, headers={"Content-Type": "application/json"}, timeout=60
)
print("without:", without.status_code)
Node (fetch)
const body = JSON.stringify({ records: [] });
// fetch() does not send Expect, but you can set it explicitly to test a hop.
const res = await fetch("https://api.example.com/v1/import", {
method: "POST",
headers: { "Content-Type": "application/json", Expect: "100-continue" },
body,
});
console.log(res.status);
if (res.status === 417) {
const retry = await fetch("https://api.example.com/v1/import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
});
console.log("retry without Expect:", retry.status);
}Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Other 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request