HTTP 412 is Precondition Failed: a condition in the request’s headers evaluated to false on the server, so the method wasn’t applied (RFC 9110 section 15.5.13). It’s how a client says “only do this if the resource is still the way I last saw it”, and how the server says “it isn’t”.
Optimistic locking
GET /docs/42 HTTP/1.1
HTTP/1.1 200 OK
ETag: "v7"
PUT /docs/42 HTTP/1.1
If-Match: "v7"
Content-Type: application/json
{"title": "Q3 plan"}
HTTP/1.1 412 Precondition Failed
Someone saved version 8 in between, so the server refuses instead of silently overwriting it: the lost-update problem. The client fetches the document again, reapplies its change and retries with the new ETag.
The headers that produce it
If-Match: "v7" 412 unless the current ETag is "v7"
If-Match: * 412 if the resource doesn't exist
If-None-Match: * (on a PUT) 412 if it already exists: create it, never overwrite
If-Unmodified-Since: <date> 412 if it changed since; ignored when If-Match is present
RFC 9110 section 13.2.2 fixes the order: If-Match first, If-Unmodified-Since only without it, then If-None-Match, which yields 304 on a GET or HEAD and 412 on any other method. If-Match uses strong comparison, so a weak ETag like W/"v7" never matches it.
412, 428, 409 or 304
a conditional GET found your copy still fresh 304 Not Modified
the condition you sent was false 412 Precondition Failed
you sent no condition, and the server requires one 428 Precondition Required
the request clashes with the state some other way 409 Conflict
Not everyone uses 412 for stale versions: Kubernetes answers an update with an outdated resourceVersion with 409 Conflict.
Who sends it
- Amazon S3 supports conditional writes:
If-None-Match: *on a PUT fails with 412 when the key exists, andIf-Matchfails it when the object’s ETag has changed. - Google Cloud Storage fails
x-goog-if-generation-matchand its other preconditions with 412. - Azure Blob Storage answers a failed
If-Matchwith 412. - Go’s
http.ServeContentevaluatesIf-MatchandIf-Unmodified-Sinceand answers 412 itself.
Gotchas
- A retried write. RFC 9110 lets the server answer success instead of 412 when it can tell the same change already succeeded, such as a PUT whose response was lost.
- If-Range never produces 412. A failed
If-Rangemakes the server ignoreRangeand send the whole representation as a 200. - Make check and write atomic, for example
UPDATE ... WHERE version = 7, or two writers can both pass the check. - Caching. 412 isn’t heuristically cacheable.
# Create-only upload: fails with 412 if the object is already there
curl -X PUT -H 'If-None-Match: *' --data-binary @q3.pdf https://storage.example.com/reports/q3.pdf
Search 412 428 in the tool below to compare the two.