HTTP 412 Precondition Failed
Quick reference
| Code | 412 |
|---|---|
| Name | Precondition Failed |
| Category | 4xx Client Error |
| Spec | RFC 9110 §15.5.13 |
| Cacheable? | No |
What 412 means
HTTP 412 Precondition Failed is returned when a client sends a conditional request — one that includes a header like If-Match, If-None-Match, If-Modified-Since, or If-Unmodified-Since — and the server evaluates that condition and finds it to be false. The server will not process the request.
Conditional requests are how HTTP implements optimistic concurrency control: the client reads a resource, records its ETag or last-modified timestamp, makes changes locally, then submits an update that says "apply this change only if the resource has not been modified since I read it." If another writer has modified the resource in the meantime, the condition fails and 412 is returned rather than silently overwriting the other writer's changes. This prevents the "lost update" problem.
RFC 9110 defines the evaluation rules in §13: If-Match requires the current ETag to match the client's supplied value. If-Unmodified-Since requires the resource not to have been modified after the supplied date. If-None-Match: * requires the resource to not exist. Each condition is evaluated against the resource's current representation before the request method is applied.
The three main conditional request patterns
Pattern 1 — Optimistic locking with If-Match (PUT/PATCH). The most common use. Client reads a resource and records its ETag, edits it locally, then submits the update with If-Match to prevent overwriting concurrent changes:
# Step 1: read the resource and note its ETag
GET /api/articles/42 HTTP/1.1
HTTP/1.1 200 OK
ETag: "v3-a1b2c3d4"
Content-Type: application/json
{"id": 42, "title": "Hello World", "views": 150}
# Step 2: submit update with the recorded ETag
PATCH /api/articles/42 HTTP/1.1
If-Match: "v3-a1b2c3d4"
Content-Type: application/json
{"title": "Hello Updated World"}
# If article was updated by someone else between steps 1 and 2:
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
{"error": "conflict", "message": "Resource was modified. Fetch latest and retry."}
On 412, the client should fetch the latest version, merge changes if needed, and resubmit with the new ETag.
Pattern 2 — Safe creation with If-None-Match: * (PUT). Ensures a resource does not already exist before creating it. Prevents duplicate creation race conditions:
PUT /api/users/alice HTTP/1.1
If-None-Match: *
Content-Type: application/json
{"name": "Alice", "email": "alice@example.com"}
# If /api/users/alice already exists:
HTTP/1.1 412 Precondition Failed
Pattern 3 — If-Unmodified-Since (legacy/cache validation). A date-based alternative to If-Match. Less precise because HTTP dates have 1-second resolution, making it possible for two modifications within the same second to bypass the check. ETags are preferred for concurrent write protection.
DELETE /api/sessions/xyz HTTP/1.1 If-Unmodified-Since: Fri, 25 Apr 2026 12:00:00 GMT # If session was modified (touched) after that date: HTTP/1.1 412 Precondition Failed
Server-side implementation
Implementing 412 handling on the server requires: (1) storing ETags with resources, (2) comparing the request's conditional headers against current state before applying the method, and (3) returning 412 when the condition is false.
Express.js example:
app.patch('/api/articles/:id', async (req, res) => {
const article = await db.articles.findById(req.params.id);
if (!article) return res.status(404).json({ error: 'not_found' });
const ifMatch = req.headers['if-match'];
if (ifMatch && ifMatch !== article.etag) {
return res.status(412).json({
error: 'precondition_failed',
current_etag: article.etag
});
}
const updated = await db.articles.update(req.params.id, req.body);
res.set('ETag', updated.etag);
res.json(updated);
});
412 vs related codes
| Code | Meaning | Key difference |
|---|---|---|
| 412 | Conditional header evaluated false | Client provided a condition; it failed |
| 428 Precondition Required | Conditional header missing | Client did not provide any condition at all |
| 409 Conflict | State conflict prevents the operation | Not tied to conditional headers; broader conflict |
| 304 Not Modified | Conditional GET — resource unchanged | GET only; success path for conditional reads |
Frequently asked questions
What is the difference between 412 and 409?
412 is a specific subset of conflict: the client explicitly stated a precondition (If-Match, If-Unmodified-Since) and the current server state violates it. 409 is broader — it means the request would create a conflict with the current server state, but no conditional header was evaluated. Use 412 when you evaluated a client-provided precondition and it failed; use 409 for other state-based conflicts.
Should the 412 response body include the current ETag?
Yes, if possible. Including the current ETag in the response body (or in the ETag response header) allows the client to immediately see the current state without making an extra GET request. This makes retry logic simpler and reduces round trips.
Is 412 safe to retry automatically?
Not without user or application logic. Automatically retrying a 412 with the same body would just fail again — the precondition is still false. The client must fetch the current resource, resolve any conflicts, and resubmit with the new ETag before the retry can succeed.
Can 412 occur on GET requests?
Yes, but only with If-Match or If-Unmodified-Since on a GET. In practice, GET requests use If-None-Match and If-Modified-Since, which return 304 Not Modified on success and 200 with fresh content on failure — they do not produce 412. Only the "if-match" and "if-unmodified-since" headers produce 412 when their conditions fail.