Skip to content

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.

Facts about this status code
Class4xx, Client errors
Defined inRFC 9110 §15.5.13
Cacheable by defaultOnly with explicit Cache-Control or Expires
Safe to retryNot blindly; fetch the current version, reapply the change, then retry with the new ETag
Relevant headers
  • If-Match: request header; fails with 412 when the current ETag differs
  • If-Unmodified-Since: request header; fails when the resource changed after that date
  • If-None-Match: on PUT or POST, If-None-Match: * fails with 412 if the resource already exists
  • ETag: send the current one in the 412 so the client can resync

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.

Express (Node.js)
app.put('/documents/:id', (req, res) => {
  res.set('ETag', '"v8"');
  res.status(412).json({ error: 'Document changed since you loaded it' });
});
Next.js App Router route handler
// app/documents/[id]/route.ts
export async function PUT() {
  return Response.json(
    { error: 'Document changed since you loaded it' },
    { status: 412, headers: { 'ETag': '"v8"' } }
  );
}
Go net/http
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"}`))
})
Python FastAPI
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.