Skip to content
Control Plane Labs

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/import

What 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