Views: 105

412 Precondition Failed: Stale ETag, Fix If-Match

412 Precondition Failed means your If-Match ETag no longer matches the server version. Spot the stale write and CDN ETag trap. Free instant check, no sign-up.

Check your domain for this issue now

Free, no sign-up. Runs the exact check this guide describes and shows what to fix.

Problem

A conditional PUT, PATCH, or DELETE comes back 412 Precondition Failed. The request was valid, auth passed, and the route works. But you attached a condition — an If-Match ETag or an If-Unmodified-Since date — and the server tested that condition against the resource’s current state and it came back false. Someone or something changed the resource since you last read it.

Symptoms

  • A save fails with “the record was modified by someone else” or “version conflict — please refresh.”
  • A PUT with If-Match: "..." returns 412 while a plain PUT (no condition) succeeds and overwrites.
  • Preconditions pass against the origin directly but fail the moment traffic goes through a CDN or reverse proxy.
  • A create-only PUT with If-None-Match: * returns 412 because the resource already exists.

What 412 Actually Means

RFC 9110 (§15.5.13) defines 412 as the case where “one or more conditions given in the request header fields evaluated to false when tested on the server.” The key word is conditions: 412 never happens by accident. It only appears when your request carried a precondition header, and the server is telling you the precondition didn’t hold.

This is the machinery of optimistic concurrency control, and it exists to solve exactly one problem: the lost update. You GET a resource and the server hands back ETag: "v1". You edit it. You PUT your change with If-Match: "v1", which means “only apply this if the resource is still the version I read.” If a second client saved "v2" in the gap, the server’s ETag is now "v2", your precondition fails, and you get 412 — instead of silently stomping the other person’s work. That 412 is the system working. It caught a race you’d otherwise never have noticed.

The spec pins down the evaluation order too (§13.2.2): If-Match and If-Unmodified-Since take precedence over If-None-Match and If-Modified-Since. That ordering matters when a request carries more than one condition, and it’s why a stale If-Match short-circuits everything else. The whole design is a promise: no unsafe write lands unless the version you targeted is still the version that’s there.

Top 3 Causes

  1. A stale ETag from a genuine race - The textbook case, and usually correct behavior. You read version "v1", someone else wrote "v2" while you were editing, and your If-Match: "v1" no longer matches. The server refuses to clobber the newer data and returns 412. There is no bug here — the fix is to re-GET, take the current ETag, merge your change onto the fresh state, and resubmit. If you find yourself annoyed by this 412, remember what it prevented: your edit quietly erasing someone else’s.

  2. A proxy rewriting the ETag - The infuriating case. Apache’s mod_deflate appends -gzip to ETags when it compresses a response, so the client caches "abc-gzip" while the origin’s canonical ETag is "abc". Your If-Match sends the mangled value, the origin compares it against the real one, and it never matches — 412 on every conditional write, even with zero concurrent activity. Some CDNs regenerate or strip ETags entirely. The tell is that preconditions work against the origin and break through the edge.

  3. Date-based preconditions and clock skew - If-Unmodified-Since compares timestamps at one-second granularity, which is coarse and fragile. If the resource was modified in the same second you read it, or the server’s Last-Modified clock drifts from the client’s, the comparison can fail even when nothing meaningful changed. ETags are strong identifiers; HTTP dates are a lossy approximation. When you can, condition on If-Match with an ETag rather than If-Unmodified-Since with a date.

Diagnose with DechoNet

  • HTTP Check to see the ETag and Last-Modified headers the endpoint actually returns to a clean GET. A 412 is a mismatch between the validator you’re sending and the validator the resource currently has, so the first move is to read the current one. If the ETag the check reports doesn’t match what your client has cached, that’s your answer — you’re conditioning on a version that’s gone. If the check shows no ETag at all, the endpoint may not support conditional requests the way your client assumes.

Resolution Checklist

  • Compare the ETag you’re sending in If-Match against the ETag the resource returns right now. If they differ, the resource moved — re-read before you write.
  • For a real stale-write race, re-GET the resource, take the fresh ETag, reapply your change onto the current state, and resubmit with the new If-Match. Never loop the same stale condition — it will 412 forever.
  • If you’re behind Apache with compression, check for the -gzip ETag suffix. Either disable the suffix (DeflateAlterETag NoChange on modern Apache) or strip it client-side before sending If-Match.
  • If preconditions pass at the origin but fail through a CDN, inspect what ETag the edge is serving versus what the origin sets. A proxy that regenerates ETags breaks conditional requests by design — key it to pass the origin’s value through untouched.
  • Prefer If-Match with a strong ETag over If-Unmodified-Since with a date. Second-granularity timestamps cause spurious 412s that an entity tag would avoid.
  • Re-run the conditional request with the current validator and confirm it now returns 2xx.

When to Escalate

  • If clients hit 412 on every conditional write with no concurrency, the ETag pipeline is broken somewhere between origin and client — usually a proxy rewriting or stripping the tag. That’s an infrastructure fix, and it belongs with whoever owns the edge.
  • If a resource that’s meant to support optimistic concurrency returns 412 on legitimate first writes, check whether an upstream cache is serving a stale ETag from a previous version. Cached validators that outlive the resource state turn every conditional write into a false conflict.

Related Tools

Related Guides

Share this guide

[Ad] Guide Detail Inline
← Back to All Guides