Skip to content

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.

Facts about this status code
Class4xx, Client errors
Defined inRFC 6585 §3
Cacheable by defaultNo; RFC 6585 says caches must not store it
Safe to retryYes, after adding If-Match (or If-Unmodified-Since) with the current ETag
Relevant headers
  • If-Match: the conditional header the server wants, carrying the ETag you last read
  • If-Unmodified-Since: date-based alternative when the resource has no ETag
  • ETag: sent by the server on GET; the value to copy into If-Match

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

Express (Node.js)
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
});
Next.js App Router route handler
// 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 }
  );
}
Go net/http
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"}`))
})
Python FastAPI
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.