HTTP status code · Client errors (4xx)
428 Precondition Required
428 Precondition Required means the server refuses to apply a change unless the request is conditional, usually with an If-Match header. The cause is a PUT, PATCH or DELETE sent without the ETag the client got when it last read the resource.
| Class | 4xx, Client errors |
|---|---|
| Defined in | RFC 6585 §3 |
| Cacheable by default | No; RFC 6585 says caches must not store it |
| Safe to retry | Yes, after adding If-Match (or If-Unmodified-Since) with the current ETag |
| Relevant headers |
|
What 428 means
RFC 6585, section 3, created 428 to prevent the lost update problem: two clients GET the same document, both edit it, and the second PUT silently overwrites the first. By demanding If-Match, the server makes every writer prove it is editing the latest version.
The RFC asks the response to explain how to resubmit, and forbids caches from storing it. 428 is the "you forgot the precondition" answer; if the precondition is present but stale, the answer is 412 instead.
Common causes
If you run the server
- The client library never sends conditional headers, because it was written against an API that did not require them.
- A proxy or client drops the ETag from GET responses, so the client has nothing to put in If-Match.
- A script updates records with blind PUTs without reading them first.
How to fix it
If you run the server
- GET the resource, keep the ETag response header, and send it back as If-Match on the write.
- If the write then returns 412, someone changed the resource in the meantime: fetch it again, reapply your change and retry.
- On the server, include a short explanation in the 428 body and document which methods require If-Match.
How to send 428
app.put('/articles/:id', (req, res) => {
if (!req.get('If-Match')) {
return res.status(428).json({ error: 'Send If-Match with the ETag from your last GET' });
}
// compare If-Match with the current ETag; on mismatch answer 412
});// app/articles/[id]/route.ts
export async function PUT() {
return Response.json(
{ error: 'Send If-Match with the ETag from your last GET' },
{ status: 428 }
);
}mux.HandleFunc("PUT /articles/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusPreconditionRequired) // 428
w.Write([]byte(`{"error":"Send If-Match with the ETag from your last GET"}`))
})from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.put("/articles/{id}")
def update_article(id: str):
raise HTTPException(status_code=428, detail="Send If-Match with the ETag from your last GET")Commonly confused with
- 428 vs 412
- 412 means you sent a precondition and it failed (the ETag no longer matches); 428 means you sent no precondition at all.
- 428 vs 409
- 409 reports a conflict the server detected by its own logic; 428 asks the client to make conflicts detectable by sending If-Match.
Frequently asked questions
- How do I fix 428 Precondition Required?
- Read the resource first, copy the ETag header from that response, and resend your PUT, PATCH or DELETE with If-Match: "<that etag>". Some APIs also accept If-Unmodified-Since with the Last-Modified date.
- What is the lost update problem?
- Two people load the same record, both edit, and the second save silently wipes out the first. Requiring If-Match turns the second save into a 412, so the client knows it must reload and merge.
- Can a 428 response be cached?
- No. RFC 6585 states that responses with 428 must not be stored by a cache, since the right answer depends on the headers of each request.
Last reviewed by Arielton Oberek.