428 Precondition Required: Add If-Match to Fix
428 Precondition Required means the server refuses your PUT/PATCH without an If-Match ETag. GET the ETag, resend in 3 steps. Free instant check, no sign-up.
Check your domain for this issue now
Free, no sign-up. Runs the exact check this guide describes and shows what to fix.
Problem
A PUT, PATCH, or DELETE comes back 428 Precondition Required. Auth passed, the route is correct, the body validates — but the write is refused because you sent it unconditionally. The server has a policy: it will not accept a mutation that isn’t pinned to a specific version of the resource, and your request carried no If-Match (or If-Unmodified-Since) header to pin it. A plain GET on the same resource works fine, because reads don’t need a precondition.
Symptoms
- A
PATCHorPUTreturns 428 whileGETon the same endpoint returns 200. - The error body says something like “an If-Match header is required” or “precondition required.”
- The identical request starts working the moment you add
If-Match: "..."with an ETag. - Only writes fail — reads, health checks, and unrelated endpoints are all fine.
- An OData / SAP / enterprise API rejects every update until you round-trip the entity’s ETag.
What 428 Actually Means
RFC 6585 (§3) defines 428 as the origin server requiring the request to be conditional: “its typical use is to avoid the ‘lost update’ problem, where a client GETs a resource’s state, modifies it, and PUTs it back to the server, when meanwhile a third party has modified the state on the server, leading to a conflict.” The response SHOULD explain how to resubmit successfully — usually by naming the header it wants.
This is optimistic concurrency control made mandatory. The lost update is the whole reason it exists: two clients read version "v1", both edit, both write back. Without a precondition, the second write silently overwrites the first, and the first client’s change vanishes with no error. A server that returns 428 is refusing to play that game. It demands that every write declare which version it intends to replace — If-Match: "v1" — so it can reject the write if the resource has moved on since you read it.
428 is the mirror image of 412 Precondition Failed. 412 fires when a precondition was present and evaluated false. 428 fires when a precondition was required and absent. And unlike 412, 428 is almost never emitted by nginx or Apache out of the box — it’s an application-level decision. Some framework, API gateway, or handler was explicitly written to demand conditional writes. The best real-world examples are enterprise data APIs: SAP S/4HANA’s OData services return 428 on any PATCH that doesn’t carry the entity’s ETag as If-Match.
Top 3 Causes
-
The API enforces optimistic concurrency by design - The intended path, not a bug. Platforms like SAP OData and Salesforce require every mutation to be conditional. You’re supposed to
GETthe resource, read itsETag, and echo it back asIf-Matchon the write. The 428 is the server teaching you its contract: no unpinned writes allowed. Once you round-trip the validator, the write goes through. -
The client never sent the header - You meant to include
If-Match, but it didn’t arrive. A client library that doesn’t forward custom headers, a reverse proxy strippingIf-Matchon the way through, or a copy-paste request that dropped the header all produce a 428 that looks like a server problem but is a missing header. The tell: the request works from a REST client where you set the header by hand. -
No validator to send - You went to attach
If-Match, but theGETnever gave you an ETag to use. If the resource’sGETresponse has noETagorLast-Modified— because the endpoint doesn’t emit one, or a CDN stripped it — you have nothing to condition on, and every write dead-ends at 428. This is a broken contract on the server side: it demands a precondition while refusing to supply the validator that would satisfy it.
Diagnose with DechoNet
- HTTP Check to confirm the endpoint actually returns a validator on
GET. You cannot satisfy a 428 without one —If-Matchneeds an ETag, andIf-Unmodified-Sinceneeds aLast-Modified. If the check shows theGETresponse carries a realETag, capture it and echo it back on your write; that’s the whole fix. If theGETreturns noETagand noLast-Modifiedat all, the concurrency contract is broken — the server is demanding a precondition it never gives you the means to build, and that’s a server-side bug, not a client one.
Resolution Checklist
-
GETthe resource and capture theETagfrom the response headers (orLast-Modifiedif that’s all it offers). - Resend the write with
If-Match: "<etag>"(orIf-Unmodified-Since: <date>) and confirm it now returns 2xx. - If the
GETreturns no validator, check whether a CDN or proxy is stripping theETag— you cannot satisfy 428 without one. If the origin genuinely emits none, that’s a server-side fix. - Reach for
If-Match: *only when you truly mean “write if it exists, any version” — it clears the 428 but disables the lost-update guard the code was enforcing. - If adding
If-Matchturns the 428 into a 412, your ETag is stale. Re-GET, take the fresh validator, and retry — don’t loop the old one. - Verify the fix under real conditions: the write should succeed with a current ETag and fail cleanly (412) with a stale one. If both succeed, the precondition isn’t actually being enforced.
When to Escalate
- If the
GETreturns noETagorLast-Modifiedand the API still demands 428 on writes, that’s a contract bug: the server can’t logically be satisfied. Fixing it means the API team emits a validator on reads — it’s not something a client can work around short ofIf-Match: *, which defeats the purpose. - If
If-Matchworks direct-to-origin but 428 persists through the edge, a proxy is stripping the header between client and origin. That’s an infrastructure fix, and it belongs with whoever owns the gateway or CDN.
Related Tools
Related Guides
Share this guide