HTTP 428 Precondition Required

Precondition Required

HTTP 428 is Precondition Required: the origin server requires this request to be conditional (RFC 6585 section 3). Its job is preventing lost updates, where a client GETs a resource, changes it and PUTs it back while someone else changed it in between. By refusing writes that don’t say which version they’re based on, the server makes sure every client edits the current copy. The response should explain how to resubmit, and caches must not store it.

The exchange

PUT /docs/42 HTTP/1.1
Content-Type: application/json

{"title": "Q3 plan"}

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

{"title": "Precondition required", "status": 428, "detail": "Send If-Match with the ETag from your last GET."}

The sequence the server wants:

GET /docs/42 HTTP/1.1

HTTP/1.1 200 OK
ETag: "v7"

PUT /docs/42 HTTP/1.1
If-Match: "v7"
Content-Type: application/json

{"title": "Q3 plan"}

HTTP/1.1 200 OK
ETag: "v8"

If someone else saved in between, the conditional PUT gets 412 Precondition Failed instead, and the client reloads before trying again.

428 or 412

no If-Match or If-Unmodified-Since at all       428 Precondition Required
If-Match sent, but the ETag has moved on        412 Precondition Failed

Gotchas

  • Prefer ETags to dates. RFC 6585 doesn’t say which precondition to require, but If-Unmodified-Since has one-second resolution, so two saves in the same second look alike; If-Match with an ETag doesn’t have that problem.
  • Strong ETags only. If-Match uses strong comparison, so a weak ETag like W/"v7" never matches and every write fails with 412.
  • Make the check and the write one step, such as UPDATE docs SET ... WHERE id = 42 AND version = 7, or two clients can pass the check at once.
  • Older clients that don’t know 428 treat it as a plain 400, as RFC 9110 says to do with any unknown 4xx. Say in the body what to send.
  • Caching. RFC 6585 forbids caches from storing a 428.

In your stack

app.put('/docs/:id', async (req, res) => {                  // Express
  const ifMatch = req.get('If-Match');
  if (!ifMatch) {
    return res.status(428).json({ detail: 'Send If-Match with the ETag from your last GET.' });
  }
  const version = Number(ifMatch.replace(/^"v|"$/g, ''));    // a list or * needs a real parser
  const saved = await docs.saveIfVersion(req.params.id, req.body, version);
  if (!saved) {
    return res.status(412).json({ detail: 'The document changed; fetch it again.' });
  }
  res.set('ETag', `"v${saved.version}"`).json(saved);
});

Here saveIfVersion stands for a single conditional update that returns nothing when the stored version has moved on.

Search 412 428 in the tool below to compare the two.

Try it in the HTTP Status Codes

428 Precondition Required

  1. 428Precondition Required4xx Client error

    The server wants the request to be conditional — an If-Match — before it will change anything.

    When to use it

    Guarding writes against lost updates, so a client cannot overwrite a change it never saw.

    Common mistakes

    Returning it without saying which precondition is required, which leaves the client guessing.

    Not cacheable by defaultRFC 6585

Every code, by class

Codes marked unofficial come from one server’s own convention — nginx, Cloudflare, cPanel, Microsoft — and are not in the IANA registry.

Runs in your browser. Nothing you enter is sent anywhere.