HTTP 428 Precondition Required
HTTP 428 Precondition Required means the server is refusing to process a write request because the client did not include a conditional header. The server requires proof that the client has seen the current version of the resource before it will apply the change. The most common implementation: a PUT or PATCH endpoint that requires If-Match with the current ETag to prevent the lost-update problem in concurrent environments.
Quick reference
| Code | 428 |
|---|---|
| Name | Precondition Required |
| Category | 4xx Client Error |
| Specification | RFC 6585 §3 |
| Fix | Include If-Match with the current ETag (GET the resource first) |
| Cacheable? | No |
The lost-update problem
Without conditional requests, concurrent writes silently overwrite each other. The classic lost-update scenario:
Time 0: Client A GET /api/documents/42 → {"version": 3, "text": "Hello"}
Time 0: Client B GET /api/documents/42 → {"version": 3, "text": "Hello"}
Time 1: Client A PUT /api/documents/42 body: {"text": "Hello World"}
→ 200 OK (document now at version 4, text: "Hello World")
Time 2: Client B PUT /api/documents/42 body: {"text": "Hello Everyone"}
→ 200 OK (document now at version 5, text: "Hello Everyone")
Client A’s change is silently gone.
A server returning 428 on unconditional PUT/PATCH requests prevents this by forcing clients to prove they have seen the current version before writing:
Client B PUT /api/documents/42
If-Match: "v3" ← Client B’s ETag from the GET
→ 412 Precondition Failed (resource is now at v4, If-Match fails)
Client B must re-GET to see the current state before retrying.
Complete request-response flow
Step 1: Client GETs the resource and captures ETag
GET /api/orders/7829 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
ETag: "v3-f9a2"
Content-Type: application/json
{"status": "pending", "amount": 49.99}
Step 2: Client attempts write without If-Match — 428 returned
PUT /api/orders/7829 HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"status": "processing"}
HTTP/1.1 428 Precondition Required
Content-Type: application/json
{"error": "precondition_required",
"message": "Include If-Match header with current ETag"}
Step 3: Client includes If-Match — success
PUT /api/orders/7829 HTTP/1.1
Host: api.example.com
Content-Type: application/json
If-Match: "v3-f9a2"
{"status": "processing"}
HTTP/1.1 200 OK
ETag: "v4-b81e"
Step 4: If another client updated first — 412 instead of 200
HTTP/1.1 412 Precondition Failed (resource was updated by someone else; ETag is now v4-b81e, not v3-f9a2) Client must re-GET to see current state before retrying.
Server-side implementation
Express.js:
app.put('/api/orders/:id', async (req, res) => {
if (!req.headers['if-match']) {
return res.status(428).json({
error: 'precondition_required',
message: 'Include If-Match header with current ETag'
});
}
const current = await db.getOrder(req.params.id);
const currentEtag = '"' + current.version + '"';
if (req.headers['if-match'] !== currentEtag) {
return res.status(412).json({error: 'precondition_failed'});
}
const updated = await db.updateOrder(req.params.id, req.body);
res.set('ETag', '"' + updated.version + '"').json(updated);
});
Django REST Framework: Use a custom mixin that checks for the If-Match header before allowing updates. DRF does not implement optimistic locking natively; use a library like django-concurrency or implement it manually in the update() method.
Spring Boot: Use the @Version JPA annotation for database-level optimistic locking. Return 428 when the header is missing and 412 when the precondition fails.
Which headers trigger 428
RFC 6585 does not limit 428 to any specific header. A server may require any of the conditional headers:
| Header | Use case | Value |
|---|---|---|
| If-Match | Update if ETag matches (optimistic locking) | ETag value: "v3-f9a2" |
| If-None-Match | Create if resource does not exist | * |
| If-Unmodified-Since | Update if not changed since timestamp | HTTP date: Sat, 01 Jan 2026 00:00:00 GMT |
| If-Modified-Since | Conditional GET (rarely requires 428) | HTTP date |
The server should include a response body indicating which header is required and, ideally, what value to use (the current ETag).
428 vs 412 vs 409
| Code | Meaning | When |
|---|---|---|
| 428 | Precondition Required | Client did not include a conditional header that is required |
| 412 | Precondition Failed | Client included conditional header but it did not match current state |
| 409 | Conflict | Request conflicts with current resource state (not a precondition mechanism) |
Frequently asked questions
What does HTTP 428 mean?
The server requires a conditional header (typically If-Match) in the request. Fetch the current resource first, capture its ETag from the response headers, and include If-Match: <etag> in your write request.
What is the difference between 428 and 412?
428 means the conditional header was missing entirely. 412 means the header was present but the condition failed (the resource has changed since the ETag was captured). 428 comes first in the flow — if the server requires If-Match and it is absent, you get 428. If it is present but stale, you get 412.
How do I get the ETag to use in If-Match?
Issue a GET request for the resource. The response headers will include an ETag header. Copy that value (including the quotes) into your If-Match header on the subsequent PUT or PATCH request.
Does 428 only apply to PUT and PATCH?
In practice yes — write methods that can overwrite data. DELETE requests can also be protected with If-Match. GET requests do not change state and do not require preconditions (though you can use conditional GET headers for caching optimization).
Related guides
HTTP 412 Precondition Failed · HTTP 409 Conflict · HTTP 204 No Content
Standards reference
Definitions from the IANA HTTP Status Code Registry and RFC 6585 §3. Human-readable guidance by ErrorLookup. · HTTP 428 quick reference →