Views: 102

428 Precondition Required: Add If-Match to Fix

428 Precondition Required means the server refuses your PUT/PATCH without an If-Match ETag. GET the ETag, resend in 3 steps. 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 PUT, PATCH, or DELETE comes back 428 Precondition Required. Auth passed, the route is correct, the body validates — but the write is refused because you sent it unconditionally. The server has a policy: it will not accept a mutation that isn’t pinned to a specific version of the resource, and your request carried no If-Match (or If-Unmodified-Since) header to pin it. A plain GET on the same resource works fine, because reads don’t need a precondition.

Symptoms

  • A PATCH or PUT returns 428 while GET on the same endpoint returns 200.
  • The error body says something like “an If-Match header is required” or “precondition required.”
  • The identical request starts working the moment you add If-Match: "..." with an ETag.
  • Only writes fail — reads, health checks, and unrelated endpoints are all fine.
  • An OData / SAP / enterprise API rejects every update until you round-trip the entity’s ETag.

What 428 Actually Means

RFC 6585 (§3) defines 428 as the origin server requiring the request to be conditional: “its typical use is to avoid the ‘lost update’ problem, where a client GETs a resource’s state, modifies it, and PUTs it back to the server, when meanwhile a third party has modified the state on the server, leading to a conflict.” The response SHOULD explain how to resubmit successfully — usually by naming the header it wants.

This is optimistic concurrency control made mandatory. The lost update is the whole reason it exists: two clients read version "v1", both edit, both write back. Without a precondition, the second write silently overwrites the first, and the first client’s change vanishes with no error. A server that returns 428 is refusing to play that game. It demands that every write declare which version it intends to replace — If-Match: "v1" — so it can reject the write if the resource has moved on since you read it.

428 is the mirror image of 412 Precondition Failed. 412 fires when a precondition was present and evaluated false. 428 fires when a precondition was required and absent. And unlike 412, 428 is almost never emitted by nginx or Apache out of the box — it’s an application-level decision. Some framework, API gateway, or handler was explicitly written to demand conditional writes. The best real-world examples are enterprise data APIs: SAP S/4HANA’s OData services return 428 on any PATCH that doesn’t carry the entity’s ETag as If-Match.

Top 3 Causes

  1. The API enforces optimistic concurrency by design - The intended path, not a bug. Platforms like SAP OData and Salesforce require every mutation to be conditional. You’re supposed to GET the resource, read its ETag, and echo it back as If-Match on the write. The 428 is the server teaching you its contract: no unpinned writes allowed. Once you round-trip the validator, the write goes through.

  2. The client never sent the header - You meant to include If-Match, but it didn’t arrive. A client library that doesn’t forward custom headers, a reverse proxy stripping If-Match on the way through, or a copy-paste request that dropped the header all produce a 428 that looks like a server problem but is a missing header. The tell: the request works from a REST client where you set the header by hand.

  3. No validator to send - You went to attach If-Match, but the GET never gave you an ETag to use. If the resource’s GET response has no ETag or Last-Modified — because the endpoint doesn’t emit one, or a CDN stripped it — you have nothing to condition on, and every write dead-ends at 428. This is a broken contract on the server side: it demands a precondition while refusing to supply the validator that would satisfy it.

Diagnose with DechoNet

  • HTTP Check to confirm the endpoint actually returns a validator on GET. You cannot satisfy a 428 without one — If-Match needs an ETag, and If-Unmodified-Since needs a Last-Modified. If the check shows the GET response carries a real ETag, capture it and echo it back on your write; that’s the whole fix. If the GET returns no ETag and no Last-Modified at all, the concurrency contract is broken — the server is demanding a precondition it never gives you the means to build, and that’s a server-side bug, not a client one.

Resolution Checklist

  • GET the resource and capture the ETag from the response headers (or Last-Modified if that’s all it offers).
  • Resend the write with If-Match: "<etag>" (or If-Unmodified-Since: <date>) and confirm it now returns 2xx.
  • If the GET returns no validator, check whether a CDN or proxy is stripping the ETag — you cannot satisfy 428 without one. If the origin genuinely emits none, that’s a server-side fix.
  • Reach for If-Match: * only when you truly mean “write if it exists, any version” — it clears the 428 but disables the lost-update guard the code was enforcing.
  • If adding If-Match turns the 428 into a 412, your ETag is stale. Re-GET, take the fresh validator, and retry — don’t loop the old one.
  • Verify the fix under real conditions: the write should succeed with a current ETag and fail cleanly (412) with a stale one. If both succeed, the precondition isn’t actually being enforced.

When to Escalate

  • If the GET returns no ETag or Last-Modified and the API still demands 428 on writes, that’s a contract bug: the server can’t logically be satisfied. Fixing it means the API team emits a validator on reads — it’s not something a client can work around short of If-Match: *, which defeats the purpose.
  • If If-Match works direct-to-origin but 428 persists through the edge, a proxy is stripping the header between client and origin. That’s an infrastructure fix, and it belongs with whoever owns the gateway or CDN.

Related Tools

Related Guides

Share this guide

[Ad] Guide Detail Inline
← Back to All Guides