412 vs 428: Precondition Failed vs Precondition Required
412 means a conditional request failed because the resource state changed. 428 means the server requires a conditional request but none was sent.
| Aspect | HTTP 412 — Precondition Failed | HTTP 428 — Precondition Required |
|---|---|---|
| RFC | RFC 9110, Section 15.5.13 | RFC 6585, Section 3 |
| Meaning | The conditional headers were evaluated; conditions were not met | The server requires conditional headers but none were sent |
| Client sent If-Match/If-Unmodified-Since? | Yes — but the ETag or date did not match | No — request had no conditional headers |
| Resource state | Resource has changed since the client last read it | Unknown — no precondition was provided to verify |
| Cacheable | No | No |
| Retry behavior | Re-read the resource (GET), get new ETag, then re-send the update | Send the request again with appropriate conditional headers |
The Lost Update Problem They Both Prevent
Both codes exist to prevent the “lost update” problem. Without conditional requests, this sequence destroys data: User A reads a resource and gets version 1. User B reads the same resource and gets version 1. User A modifies and saves version 2. User B (still working from version 1) saves version 3 — silently overwriting User A’s changes.
Conditional requests break this: User A saves with If-Match: "etag-v1". This succeeds and creates version 2. User B tries to save with If-Match: "etag-v1". The server has version 2, not version 1, so the ETag does not match. The server returns 412. User B must re-read, see User A’s changes, merge, and retry.
412: The Condition Was Evaluated and Failed
412 Precondition Failed means the client sent a conditional request (using If-Match, If-None-Match, If-Modified-Since, or If-Unmodified-Since) and the condition was not satisfied. The resource has changed since the client last fetched it.
# Client read resource when ETag was "abc"
# Another client updated it; ETag is now "xyz"
PUT /api/documents/42 HTTP/1.1
If-Match: "abc"
HTTP/1.1 412 Precondition Failed
ETag: "xyz"
{"error": "precondition_failed",
"message": "Document was modified since you last read it"}
The 412 response should include the current ETag so the client can decide whether to merge changes or overwrite.
428: Conditional Headers Were Not Sent At All
428 Precondition Required means the server enforces optimistic locking on this endpoint, but the client sent no conditional headers at all. The server refuses to process an unconditional update because doing so could silently overwrite concurrent changes.
# Client sends a PUT with no If-Match header
PUT /api/documents/42 HTTP/1.1
Content-Type: application/json
{"title": "Updated Title"}
HTTP/1.1 428 Precondition Required
Content-Type: application/json
{"error": "precondition_required",
"message": "Use If-Match with the current ETag to prevent lost updates"}
The client should GET the document, read the ETag from the response, and re-send the PUT with If-Match: "<etag>".
Decision Rule
Use 412 when the client sent a conditional header and the condition was not met. Use 428 when the server requires conditional headers but the client did not send any. Together they form a complete optimistic locking enforcement mechanism: 428 tells clients they must use ETags; 412 tells them their ETag is stale.
FAQ
Can the same endpoint return both 412 and 428?
Yes. An endpoint that enforces optimistic locking returns 428 to clients that omit conditional headers, and 412 to clients that include them but have a stale ETag. This is the correct implementation of server-side lost-update prevention.
Is 428 widely supported?
It is defined in RFC 6585 and officially registered, but many APIs choose to return 400 or 409 instead, with a custom error message. 428 is semantically precise but requires clients to specifically handle it. Document whichever you use.
What is the difference between 412 on GET vs PUT?
On a GET, 412 is most commonly triggered by If-Match or If-Unmodified-Since — the client only wants the resource if it has not changed. On a PUT or DELETE, it is triggered by the same headers to prevent overwriting concurrent changes. The semantics differ by context.
Full Guides
HTTP 428 Precondition Required — full guide · All comparisons