Precondition Required
ActiveHTTP 428 Precondition Required means the server refuses to process the request because it lacks a conditional header (If-Match, If-None-Match, or If-Unmodified-Since). Defined in RFC 6585 §3. The server enforces this policy to prevent lost update problems where concurrent writers silently overwrite each other.
Description
The 428 Precondition Required status code indicates that the origin server requires the request to be conditional. Its typical use is to avoid the 'lost update' problem, where a client GETs a resource's state, modifies it, and PUTs it back to the server, when meanwhile a third party has modified the state on the server, leading to a conflict.
By requiring requests to be conditional, the server can assure that clients are working with the correct copies. A conditional request is one that includes headers like If-Match (with an ETag value), If-None-Match, or If-Unmodified-Since. Without these headers, the server cannot determine whether the client's view of the resource is stale.
428 is fundamentally different from 412 Precondition Failed. 428 means 'you MUST send a conditional header — I won't accept unconditional writes' (a server policy). 412 means 'you DID send a conditional header and it evaluated to false' (a runtime check). Think of 428 as the bouncer checking for ID at the door, and 412 as the bouncer saying your ID is expired.
The intended workflow is optimistic concurrency: the client GETs the resource (receiving an ETag), makes local modifications, then PUTs or PATCHes with If-Match set to the received ETag. If another client modified the resource in between, the server returns 412. If the client forgets the If-Match entirely, the server returns 428. This forces all clients into a safe concurrency pattern and eliminates the 'last writer wins' anti-pattern.
This status code should only be applied to unsafe methods (PUT, PATCH, DELETE) — never to safe methods like GET or HEAD. The response SHOULD include how to resubmit the request successfully, typically by explaining which conditional header is required and how to obtain the necessary value (e.g., 'perform a GET to obtain the current ETag').
Examples
PUT /api/documents/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123
{"title": "Updated Title", "body": "New content for the document."}HTTP/1.1 428 Precondition Required
Content-Type: application/json
{"error": "precondition_required", "message": "This endpoint requires a conditional request. Include an If-Match header with the resource's current ETag.", "hint": "Perform a GET on /api/documents/42 to obtain the current ETag value.", "required_headers": ["If-Match"]}PATCH /api/users/89/profile HTTP/1.1
Host: api.example.com
Content-Type: application/merge-patch+json
Authorization: Bearer user-token
{"display_name": "Alice Updated"}HTTP/1.1 428 Precondition Required
Content-Type: application/json
{"error": "precondition_required", "message": "Unconditional writes are not allowed on this resource. Please include If-Match or If-Unmodified-Since.", "documentation": "https://api.example.com/docs/concurrency", "current_etag": "\"v7-abc123\""}PUT /api/documents/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123
If-Match: "v7-abc123"
{"title": "Updated Title", "body": "New content for the document."}HTTP/1.1 200 OK
Content-Type: application/json
ETag: "v8-def456"
{"id": 42, "title": "Updated Title", "body": "New content for the document.", "version": 8, "updated_at": "2024-03-15T14:22:00Z"}Edge Cases
- •428 should only be returned for unsafe methods (PUT, PATCH, DELETE). Returning 428 on GET or HEAD is incorrect — safe methods don't modify state and don't need concurrency protection.
- •Some APIs return 428 selectively — only on resources that have concurrent access patterns. A rarely-edited config resource might accept unconditional writes while a frequently-edited document requires If-Match.
- •If the server returns 428 but doesn't provide guidance on how to obtain the required precondition value (e.g., which GET returns the ETag), the client is stuck. Always include resolution instructions in the response body.
- •Clients behind aggressive HTTP proxies or older libraries may not support custom conditional headers. Consider whether 428 enforcement will break legitimate clients in your ecosystem.
- •428 is NOT appropriate when the resource doesn't yet exist. You can't have an ETag for something that hasn't been created. Use 428 only on updates to existing resources — creation (POST) typically doesn't need preconditions.
- •Race condition: if the server helpfully includes the current ETag in the 428 response body, the client might use it directly without re-reading the resource. This bypasses the read-modify-write pattern and defeats the purpose of optimistic concurrency. The ETag in the error response is a hint, not a shortcut.
- •APIs that require If-Match on DELETE can surprise clients — many developers don't expect DELETE to need concurrency headers. Document this clearly and consider whether it's truly necessary for your use case.
When You'll See This
- →Updating a shared document in a collaborative editing API without sending If-Match
- →Modifying a CMS page that enforces optimistic locking on all write operations
- →Attempting to DELETE a resource on an API that requires If-Match for all destructive operations
- →PATCH request to a user profile endpoint that mandates conditional updates to prevent race conditions
- →Updating inventory quantities in an e-commerce system that enforces concurrency control
- →Changing a configuration resource in a distributed system where stale writes could cause inconsistency
- →Modifying a DNS zone record through an API that requires If-Unmodified-Since to prevent conflicting zone updates
Implementation References
| Language | Constant |
|---|---|
| Go | http.StatusPreconditionRequired |
| Rust | http::StatusCode::PRECONDITION_REQUIRED |
| Python | http.HTTPStatus.PRECONDITION_REQUIRED |
| Node.js | http.STATUS_CODES[428] |
| .NET | HttpStatusCode.PreconditionRequired |
| Java | 428 (no built-in constant; use literal or Spring HttpStatus.PRECONDITION_REQUIRED) |
| PHP | Response::HTTP_PRECONDITION_REQUIRED (Symfony) |
| Ruby | :precondition_required (Rack) |
History
Introduced in RFC 6585 (April 2012), 'Additional HTTP Status Codes,' alongside 429 Too Many Requests, 431 Request Header Fields Too Large, and 511 Network Authentication Required. The RFC was authored by M. Nottingham and R. Fielding to address common patterns that lacked standardized response codes. 428 specifically formalized the requirement pattern that many APIs were already implementing with non-standard 403 or 400 responses.
Related Status Codes
Related Headers
FAQ
What is the difference between 428 Precondition Required and 412 Precondition Failed?
428 and 412 are two stages of the same concurrency workflow. 428 means the client did NOT include a conditional header at all — the server's policy requires one and the request was rejected before any condition was evaluated. 412 means the client DID include a conditional header (like If-Match) but the condition evaluated to false — the resource has changed since the client last read it. In sequence: forgetting If-Match → 428. Sending a stale If-Match → 412. Both push clients toward the correct optimistic concurrency pattern: GET (receive ETag) → modify locally → PUT with If-Match.
When should an API return 428 vs just accepting unconditional writes?
Return 428 when the cost of a lost update is higher than the cost of forcing clients into a read-modify-write workflow. Collaborative editing, financial transactions, inventory management, and any resource with frequent concurrent access benefit from mandatory preconditions. For resources that are rarely updated concurrently (user preferences, static configuration), the overhead of requiring conditional requests may not be justified. The decision is a policy choice: 428 trades developer convenience for data integrity.
How should a client handle a 428 response?
The correct recovery flow is: (1) perform a GET on the resource to obtain the current representation and its ETag or Last-Modified value, (2) apply your intended modifications to the freshly-fetched state, (3) resubmit the request with the appropriate conditional header (If-Match for ETag-based, If-Unmodified-Since for date-based). This ensures the client is working from the latest state and the server can detect conflicts. Do NOT simply retry the same request — it will get 428 again. The response body usually contains hints about which header is required and how to obtain the value.