HTTP status code · Client errors (4xx)
412 Precondition Failed
412 Precondition Failed means the request carried a condition, usually If-Match with an ETag, and the resource on the server no longer matches it. The most common cause is someone else saving the same record between your read and your write.
| Class | 4xx, Client errors |
|---|---|
| Defined in | RFC 9110 §15.5.13 |
| Cacheable by default | Only with explicit Cache-Control or Expires |
| Safe to retry | Not blindly; fetch the current version, reapply the change, then retry with the new ETag |
| Relevant headers |
|
What 412 means
RFC 9110, section 15.5.13, defines 412 as one or more conditions in the request header fields evaluating to false on the server. It is the mechanism behind optimistic concurrency: read a resource, remember its ETag, and send it back in If-Match with the update. If anyone changed the resource in between, the ETag differs and the server refuses with 412 instead of silently overwriting their work, the lost update problem.
Section 13.2.2 fixes the evaluation order. If-Match comes first, then If-Unmodified-Since, then If-None-Match. A failed If-None-Match on GET or HEAD is not an error at all; it produces 304 Not Modified. On other methods it produces 412, which is how If-None-Match: * turns a PUT into create-only. Amazon S3 conditional writes use exactly that and answer 412 when the object key already exists.
Common causes
If you run the server
- Two users or two tabs edited the same record; the second save carries an ETag that is now stale.
- The client cached an ETag from an earlier response and reused it after the resource was updated by a background job.
- A create-only upload (If-None-Match: *) targeted a key that already exists, as with S3 or Google Cloud Storage conditional writes.
- A proxy or compression layer rewrote the ETag (for example, nginx turning a strong ETag weak after gzip), so If-Match never matches.
How to fix it
If you run the server
- On the client, catch 412, GET the resource again, merge or reapply the user change, and resend with the new ETag. Show the user a conflict message if the change cannot be merged automatically.
- Include the current ETag in the 412 response so the client can tell how far behind it is.
- If If-Match never matches, compare the ETag the client sends with what the origin produces; strong comparison fails on W/ prefixed ETags.
How to send 412
The examples show the response only. In a real handler, compare the If-Match header with the stored ETag first and return 412 when they differ. Go http.ServeContent evaluates If-Match and If-Unmodified-Since for you on GET.
app.put('/documents/:id', (req, res) => {
res.set('ETag', '"v8"');
res.status(412).json({ error: 'Document changed since you loaded it' });
});// app/documents/[id]/route.ts
export async function PUT() {
return Response.json(
{ error: 'Document changed since you loaded it' },
{ status: 412, headers: { 'ETag': '"v8"' } }
);
}mux.HandleFunc("PUT /documents/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("ETag", "\"v8\"")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusPreconditionFailed) // 412
w.Write([]byte(`{"error":"Document changed since you loaded it"}`))
})from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.put("/documents/{id}")
def update_document(id: str):
raise HTTPException(status_code=412, detail="Document changed since you loaded it", headers={"ETag": "\"v8\""})Commonly confused with
- 412 vs 428
- 428 means the client sent no precondition and the server requires one; 412 means it sent one and it failed.
- 412 vs 409
- 409 is a conflict found by application logic; 412 is a conflict detected purely through HTTP conditional headers.
- 412 vs 304
- A failed If-None-Match on GET yields 304, a cache hit; on a write method the same failure yields 412.
Frequently asked questions
- How do ETags prevent lost updates?
- The client sends the ETag it read in If-Match. The server applies the write only if the current ETag is identical, otherwise it returns 412, so a stale client cannot overwrite a newer version.
- Should I use 412 or 409 for an edit conflict?
- Use 412 when the conflict is detected by If-Match or If-Unmodified-Since. Use 409 when your own logic finds the conflict, for example a version number in the JSON body that does not match.
- Why does S3 return 412 Precondition Failed?
- Your request included a condition such as If-None-Match: * on PutObject, or If-Match with an ETag, and the object did not satisfy it: the key already existed or had changed.
Last reviewed by Arielton Oberek.