Precondition Failed
ActiveHTTP 412 Precondition Failed indicates that one or more conditions given in the request header fields (If-Match, If-Unmodified-Since, If-None-Match) evaluated to false on the server. Defined in RFC 9110 §15.5.13. Used for optimistic concurrency control in RESTful APIs and distributed systems.
Description
The 412 Precondition Failed status code indicates that one or more conditions given in the request header fields evaluated to false when tested on the server. This response code allows the client to place preconditions on the current resource state, preventing the request method from being applied if the target resource is in an unexpected state.
The precondition mechanism is the backbone of optimistic concurrency in HTTP. A client reads a resource (receiving its ETag or Last-Modified date), performs local modifications, then sends an update with If-Match or If-Unmodified-Since. If another client modified the resource in between, the precondition fails and the server returns 412 — preventing a blind overwrite without requiring pessimistic locks.
Critically, 412 is distinct from 409 Conflict. A 412 means the conditional headers explicitly failed — the client asked a question ('has this changed?') and the server answered 'yes, it has.' A 409 means the resource is in a state that conflicts with the operation regardless of whether conditional headers were sent. Think of 412 as a guard clause the client opted into, while 409 is an inherent state problem.
The server is not required to include the current representation or state of the resource in a 412 response. The client already knows what it expected (it sent the precondition), so the response need only confirm the precondition failed. However, including the current ETag in the response headers is common practice, enabling the client to fetch the latest version and retry.
Conditional requests with If-None-Match: * and PUT enable a 'create only if not exists' pattern — the server returns 412 if the resource already exists, preventing accidental overwrites of existing resources during creation. This pattern is widely used in distributed systems where multiple writers may attempt to create the same resource concurrently.
Examples
PUT /api/articles/281 HTTP/1.1
Host: api.example.com
Content-Type: application/json
If-Match: "a1b2c3d4"
Authorization: Bearer token-xyz
{"title": "Updated Title", "body": "New content...", "status": "published"}HTTP/1.1 412 Precondition Failed
Content-Type: application/json
ETag: "e5f6g7h8"
{"error": "precondition_failed", "message": "The resource has been modified since your last read. Your ETag: a1b2c3d4, current ETag: e5f6g7h8.", "header": "If-Match"}PUT /api/configs/feature-flags HTTP/1.1
Host: api.example.com
Content-Type: application/json
If-None-Match: *
{"dark_mode": true, "beta_access": false, "max_uploads": 10}HTTP/1.1 412 Precondition Failed
Content-Type: application/json
ETag: "existing-v2"
{"error": "precondition_failed", "message": "Resource already exists. If-None-Match: * failed because the target resource has a current representation.", "existing_resource": "/api/configs/feature-flags"}DELETE /api/cache/session-store HTTP/1.1
Host: api.example.com
If-Unmodified-Since: Wed, 15 Jan 2025 10:00:00 GMT
Authorization: Bearer admin-tokenHTTP/1.1 412 Precondition Failed
Content-Type: application/json
Last-Modified: Thu, 16 Jan 2025 14:30:00 GMT
{"error": "precondition_failed", "message": "Resource was modified after Wed, 15 Jan 2025 10:00:00 GMT. Last modification: Thu, 16 Jan 2025 14:30:00 GMT.", "header": "If-Unmodified-Since"}Edge Cases
- •412 is ONLY returned when the client explicitly sends conditional headers (If-Match, If-Unmodified-Since, If-None-Match). Without those headers, the same conflict scenario would result in a 409 or a silent overwrite — never a 412.
- •If-Match with multiple ETags (If-Match: "v1", "v2", "v3") succeeds if ANY match. 412 only fires when NONE of the provided ETags match the current resource state.
- •Weak ETags (W/"abc") cannot be used with If-Match for unsafe methods (PUT, PATCH, DELETE). A server receiving a weak ETag in If-Match for an unsafe request SHOULD return 412, not silently ignore the precondition.
- •For conditional GET/HEAD with If-None-Match, a match results in 304 Not Modified — NOT 412. The 412 semantics only apply when preconditions fail for state-changing methods or when If-Match/If-Unmodified-Since is used.
- •CDN and proxy caches may generate synthetic 412 responses if a client sends If-Match against a cached representation whose ETag has been stripped or weakened during transit. This is a common source of spurious 412s in multi-tier architectures.
- •Race condition: two clients read the same ETag, both send If-Match updates simultaneously. One succeeds (200/204), the other gets 412. The losing client must re-fetch, merge, and retry — this is the expected optimistic concurrency workflow.
- •Some servers incorrectly return 412 for validation errors or business logic failures. True 412 responses are EXCLUSIVELY about conditional header evaluation — not about request body validity or business rules.
When You'll See This
- →Optimistic concurrency: PUT with If-Match fails because another client updated the resource (ETag changed)
- →Create-if-absent: PUT with If-None-Match: * fails because the resource already exists
- →Time-gated update: PATCH with If-Unmodified-Since fails because the resource was modified more recently
- →CDN cache purge: DELETE with If-Match fails because the cached representation was already invalidated by another purge
- →Distributed lock release: DELETE with If-Match on a lock resource fails because the lock was already stolen by another process
- →Configuration deployment: PUT with If-Match fails because a concurrent deployment already updated the config
- →Batch upload deduplication: PUT with If-None-Match: * on each item prevents re-uploading files that another worker already processed
Implementation References
| Language | Constant |
|---|---|
| Go | http.StatusPreconditionFailed |
| Rust | http::StatusCode::PRECONDITION_FAILED |
| Python | http.HTTPStatus.PRECONDITION_FAILED |
| Node.js | http.STATUS_CODES[412] |
| .NET | HttpStatusCode.PreconditionFailed |
| Java | HttpURLConnection.HTTP_PRECON_FAILED |
| PHP | Response::HTTP_PRECONDITION_FAILED (Symfony) |
| Ruby | :precondition_failed (Rack) |
History
Introduced in HTTP/1.1 (RFC 2068, 1997) as part of the conditional request mechanism alongside If-Match and If-Unmodified-Since headers. Refined in RFC 2616 (1999) with clearer semantics around weak vs. strong validators. Current definition in RFC 9110 (2022) §15.5.13 consolidates and clarifies behavior, particularly around the interaction between multiple precondition headers and their evaluation order.
Related Status Codes
Related Headers
FAQ
What is the difference between 412 Precondition Failed and 409 Conflict?
412 fires exclusively when conditional request headers (If-Match, If-Unmodified-Since, If-None-Match) evaluate to false — the client explicitly asked 'has this changed?' and the server says 'yes.' 409 Conflict indicates the request conflicts with the current resource state regardless of conditional headers — duplicate unique fields, invalid state transitions, version conflicts detected through application logic rather than HTTP precondition headers. The key test: remove the conditional headers. If the request would still fail for the same reason, it's a 409. If removing the headers would cause a silent overwrite instead, 412 was correct.
How do I implement optimistic concurrency with ETags and 412?
The workflow: (1) Client GETs the resource, server returns it with ETag header (e.g., ETag: "v5-abc"). (2) Client makes local edits. (3) Client PUTs the update with If-Match: "v5-abc". (4) Server compares the If-Match ETag against the resource's current ETag. If they match, the update applies (200/204). If they differ (another client modified it), server returns 412. (5) Client handles 412 by fetching the latest version (new ETag), merging changes (three-way merge or user-prompted), and retrying with the new ETag. This provides conflict detection without pessimistic locking.
When should I use If-None-Match: * with PUT to get a 412?
Use If-None-Match: * when you want 'create only if not exists' semantics — the PUT should only succeed if the target resource does NOT currently exist. If it already exists, the server returns 412. This is critical in distributed systems where multiple processes may try to create the same resource concurrently (e.g., claiming a unique slug, initializing a singleton config, acquiring a distributed lock). Without If-None-Match: *, a PUT would blindly overwrite the existing resource. With it, exactly one creator wins and the others get 412, enabling safe concurrent creation without race conditions.