skip to content

What does HTTP status 428 Precondition Required mean, and when should a server return it rather than processing the request?

level: seniorimportance: should knowfreq 32%

answer

  1. 428 = you sent no precondition
  2. 412 = you sent a stale one
  3. not cacheable, body names the header
  4. stops sloppy client winning the race
  5. If-Match: * satisfies syntax, not safety

basics

~20 s

428 means the request was refused because it carried no precondition. A server returns it on unconditional PUT/PATCH/DELETE when it wants to force clients to send If-Match, so nobody can blindly overwrite state. The response should explain which precondition is expected.

solid answer

~50 s

412 handles the client who sent a validator that turned out to be stale. 428 handles the client who sent none at all. Without it, optimistic concurrency is opt-in: a careful client sends `If-Match` and gets protected, while a careless one omits it and quietly wins the race. That is the worst arrangement, because the badly behaved client is the one that overwrites. So a server that cares about lost updates makes preconditions mandatory: if a PUT, PATCH or DELETE on a mutable resource arrives without `If-Match` or `If-Unmodified-Since`, respond **428 Precondition Required** and do nothing. The status is explicitly defined as not cacheable, and the body should tell the client which header to supply. Clients then always fetch a current ETag before writing. Use it selectively - typically on resources with real concurrent editing. Applying it to append-only creates or to endpoints with a single writer just adds a round trip for no safety gain.

code

http · 10 lines
http
PUT /doc/7 HTTP/1.1
Content-Type: application/json

{"title":"Final"}

HTTP/1.1 428 Precondition Required
Content-Type: application/problem+json

{"title":"Precondition Required",
 "detail":"PUT /doc/7 requires an If-Match header with the current ETag"}

go deeper

for a junior

Know that 428 means the request lacked a required precondition header and nothing was changed.

for a middle

Contrast it cleanly with 412 and explain why opt-in preconditions leave the careless client winning.

for a senior

Decide per resource where enforcement is worth the extra round trip, return an actionable body, and bound the client retry loop.

for a principal

Treat it as a contract-level policy: which resources demand concurrency safety, how bulk writers are exempted, and how the requirement is communicated to consumers.

## The gap 412 leaves open Conditional writes protect a client only if that client opts in. Consider a server that honours `If-Match` faithfully: - Client A reads the resource, writes with `If-Match: "v1"` -> protected. - Client B reads nothing, or ignores the ETag, and writes with no precondition -> the server has no reason to refuse, so B wins. The result is perverse: the disciplined client can be rejected with 412, while the sloppy one always succeeds and clobbers everyone. The protocol needs a way for the server to say "unconditional writes are not acceptable here". That status is **428 Precondition Required**. ## What 428 says 428 is an origin-server-originated status meaning: your request is not being processed because it is missing a required precondition. Key properties: - It is a **client error (4xx)** - the request is malformed for this resource's policy, and retrying it unchanged will fail again. - The response is **not cacheable**, because the correct response depends entirely on request headers the client has yet to send. - The body should name the missing header so a developer can act on it - for example "this endpoint requires an If-Match header carrying the current ETag". No state is modified. The client's remedy is to GET the resource, take the `ETag`, and reissue the write with `If-Match`. ## How the three statuses divide the space For a write on a guarded resource: | Request | Server response | | --- | --- | | No precondition header at all | **428 Precondition Required** - refuse, ask for one | | `If-Match` matching the current ETag | Normal success, e.g. 200/204 with a new ETag | | `If-Match` not matching | **412 Precondition Failed** - stale, re-fetch and retry | 428 and 412 are complementary, not alternatives. A server that enforces preconditions returns both, in different circumstances. ## Where to apply it Enforcement is a per-resource design decision, not a blanket rule. Good candidates: documents or records edited concurrently by multiple users or agents; configuration objects that several operators can change; anything where a silent overwrite has real cost. Also useful for a `DELETE` where deleting a version you have not seen is dangerous. Poor candidates: `POST` to a collection that creates a new resource - there is no prior version to be stale about. Endpoints with a single logical writer, where the extra GET before every write is pure latency. And bulk or system-to-system pipelines that legitimately assert their state unconditionally, unless you are prepared to give them an explicit way to say so. ## Practical consequences Mandatory preconditions change the client contract, so document them and fail clearly. Two operational points come up in interviews: First, **the retry loop must be bounded**. A client that loops re-fetch/retry on 412 forever against a hot resource can spin; add a retry cap and surface a conflict to the user. Second, **`If-Match: *`** exists and means "the resource must currently exist, but I do not care which version". It satisfies a 428 check syntactically while providing no lost-update protection at all, so if you enforce preconditions for concurrency reasons decide deliberately whether to accept `*`. Its legitimate use is on PUT to assert "update only, never create"; the mirror form `If-None-Match: *` means "create only if it does not exist" and is how you make PUT-as-create safe against a duplicate.

  • What does If-Match: * mean, and does it satisfy a server that enforces 428?
    If-Match: * asserts only that the resource currently exists, with no claim about which version. Syntactically it is a precondition, so a naive 428 check will pass it, yet it provides zero lost-update protection. Its honest use is PUT-as-update-only; if your 428 enforcement exists for concurrency, decide explicitly whether to reject the wildcard.
  • Would you enforce 428 on POST to a collection?
    Normally no. POST to a collection creates a new subordinate resource, so there is no prior version the client could be stale about and nothing for If-Match to compare against. Enforcement belongs on updates and deletes of existing resources; for create-safety the relevant guard is If-None-Match: * on PUT.

saying these in an interview costs you the question

  • Using 428 when the client did send a validator and it was stale - that is 412
  • Thinking 428 is a server error or that the server should retry on the client's behalf
  • Caching a 428 response, when the status is defined as not cacheable
  • Enforcing preconditions on every endpoint including creates, adding a round trip with no safety benefit
  • Accepting If-Match: * as proof the client checked the version

context