< HTTP/1.1 428 Precondition Required428 Precondition Required
The server requires the request to be conditional. Learn when to use 428 Precondition Required to prevent lost updates and race conditions.
- Cacheable
- Only with explicit freshness
- Retry?
- Yes: add If-Match or another precondition
- Usually sent by
- Origin app
- Spec
- RFC 6585 §3
- Often confused with
- 412
On this page
TL;DR: Server requires conditional headers (like If-Match) to prevent lost updates in concurrent editing. Get the resource’s ETag first, then include it in your update request.
What is 428 Precondition Required?
A 428 Precondition Required status code means the server requires the request to include conditional headers before it can be processed. Think of it like a bank requiring you to provide your current account balance before approving a withdrawal—it ensures you’re working with up-to-date information and prevents conflicting changes.
This status code is used to prevent the “lost update problem” where multiple clients might modify a resource simultaneously, potentially overwriting each other’s changes.
When Does This Happen?
You’ll see a 428 Precondition Required response in these common situations:
1. Updating Shared Resources
Multiple users editing the same document
→ Server requires If-Match header
→ Ensures you have the latest version
→ Prevents overwriting others' changes
2. Critical Data Modifications
Updating financial records
→ Requires If-Unmodified-Since
→ Confirms data hasn't changed
→ Prevents race conditions
3. API Rate Limiting Protection
High-frequency API endpoint
→ Requires conditional headers
→ Prevents accidental duplicate requests
→ Protects server resources
4. Cache Validation Required
Critical resource update
→ Server demands If-None-Match
→ Ensures client has current version
→ Prevents stale data updates
5. Distributed System Coordination
Microservices updating shared state
→ Requires version headers
→ Maintains consistency
→ Prevents conflicts
Example Responses
Basic 428 Response:
HTTP/1.1 428 Precondition Required
Content-Type: application/json
Content-Length: 156
{
"error": "Precondition Required",
"message": "This request must be conditional. Please include an If-Match or If-Unmodified-Since header.",
"required_headers": ["If-Match", "If-Unmodified-Since"]
}
With ETag Information:
HTTP/1.1 428 Precondition Required
Content-Type: application/json
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
{
"error": "Precondition Required",
"message": "To update this resource, include an If-Match header with the current ETag",
"current_etag": "33a64df551425fcc55e4d42a148795d9f25f89d4",
"example": "If-Match: \"33a64df551425fcc55e4d42a148795d9f25f89d4\""
}
Detailed Guidance:
HTTP/1.1 428 Precondition Required
Content-Type: application/json
Link: <https://api.example.com/docs/conditional-requests>; rel="help"
{
"error": "Precondition Required",
"message": "This endpoint requires conditional request headers to prevent lost updates",
"details": {
"reason": "Resource can be modified by multiple clients",
"required_headers": [
{
"name": "If-Match",
"description": "Include the ETag from your last GET request",
"example": "If-Match: \"abc123\""
},
{
"name": "If-Unmodified-Since",
"description": "Include the Last-Modified date from your last GET request",
"example": "If-Unmodified-Since: Wed, 21 Oct 2025 07:28:00 GMT"
}
]
},
"documentation": "https://api.example.com/docs/conditional-requests"
}
Real-World Example
Imagine you’re updating a user profile through an API:
Initial Request (Missing Precondition):
PUT /api/users/12345 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"name": "John Doe",
"email": "john.doe@example.com",
"role": "admin"
}
428 Response:
HTTP/1.1 428 Precondition Required
Content-Type: application/json
Cache-Control: no-cache
Content-Length: 423
{
"error": "Precondition Required",
"message": "User profile updates require conditional headers to prevent conflicts",
"details": {
"reason": "This resource can be modified by the user and administrators simultaneously",
"solution": "First, GET the current resource to obtain the ETag, then include it in your PUT request"
},
"steps": [
"1. GET /api/users/12345 to retrieve current data and ETag",
"2. Include 'If-Match: <etag>' header in your PUT request",
"3. Retry your PUT request with the If-Match header"
],
"example": {
"header": "If-Match",
"value": "\"33a64df551425fcc55e4d42a148795d9f25f89d4\""
}
}
Corrected Request Flow:
Step 1: Get Current Resource
GET /api/users/12345 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Response:
HTTP/1.1 200 OK
ETag: "v1.2.3"
Last-Modified: Wed, 18 Jan 2026 10:00:00 GMT
{
"id": "12345",
"name": "John Smith",
"email": "john.smith@example.com",
"role": "user"
}
Step 2: Update With Precondition
PUT /api/users/12345 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
If-Match: "v1.2.3"
{
"name": "John Doe",
"email": "john.doe@example.com",
"role": "admin"
}
Response:
HTTP/1.1 200 OK
ETag: "v1.2.4"
{
"id": "12345",
"name": "John Doe",
"email": "john.doe@example.com",
"role": "admin",
"updated_at": "2026-01-18T10:05:00Z"
}
428 vs Other Conditional Codes
| Code | Meaning | Scenario | Solution |
|---|---|---|---|
| 428 | Precondition Required | No conditional header provided | Add If-Match or If-Unmodified-Since |
| 412 | Precondition Failed | Conditional header provided but failed | Get latest version and retry |
| 409 | Conflict | Request conflicts with current state | Resolve conflict manually |
| 304 | Not Modified | Conditional GET shows no change | Use cached version |
Important Characteristics
Prevents Lost Updates:
User A reads resource (version 1)
User B reads resource (version 1)
User A updates to version 2 ✓
User B updates (would overwrite A's changes)
→ 428 forces B to get version 2 first
Server Policy Decision:
HTTP/1.1 428 Precondition Required
↑
Server decides which endpoints require conditions
Common Required Headers:
If-Match: Requires specific ETagIf-Unmodified-Since: Requires resource unchanged since dateIf-None-Match: For conditional creation- Custom version headers
Common Mistakes
❌ Not providing guidance
HTTP/1.1 428 Precondition Required
Precondition Required ← Unhelpful, what precondition?
❌ Requiring conditions on read-only requests
GET /api/users/123
HTTP/1.1 428 Precondition Required ← Bad: GET shouldn't require conditions
❌ Not documenting which headers are needed
{
"error": "Precondition Required"
← Missing: which headers? which values?
}
✅ Correct usage
HTTP/1.1 428 Precondition Required
Content-Type: application/json
{
"error": "Precondition Required",
"message": "Include If-Match header with current ETag",
"required_header": "If-Match",
"get_etag_from": "GET /api/users/123",
"current_etag": "\"abc123\""
}
Getting 428 Precondition Required right
Provide Current ETag:
app.put('/api/resource/:id', async (req, res) => {
if (!req.headers['if-match']) {
const resource = await db.getResource(req.params.id)
return res.status(428).json({
error: 'Precondition Required',
message: 'Include If-Match header to update this resource',
current_etag: resource.etag,
example: `If-Match: "${resource.etag}"`
})
}
// Process conditional update...
})
Document Required Headers:
HTTP/1.1 428 Precondition Required
Content-Type: application/json
{
"error": "Precondition Required",
"required_headers": {
"If-Match": {
"description": "ETag from your last GET request",
"example": "If-Match: \"v1.2.3\"",
"obtain_from": "GET /api/resource/:id"
}
},
"documentation": "https://docs.example.com/conditional-requests"
}
Apply to Critical Endpoints Only:
const requiresPrecondition = [
'PUT /api/users/:id',
'PATCH /api/documents/:id',
'DELETE /api/critical-data/:id'
]
// Don't require for less critical operations
const optional = ['POST /api/logs', 'GET /api/*', 'POST /api/analytics']
Consistent Error Format:
function preconditionRequiredError(resourceType, etag) {
return {
error: 'Precondition Required',
message: `${resourceType} updates require conditional headers`,
required_headers: ['If-Match', 'If-Unmodified-Since'],
current_etag: etag,
steps: [
`1. GET the current ${resourceType}`,
'2. Note the ETag header in the response',
'3. Include If-Match: <etag> in your update request'
]
}
}
Implementation Examples
Express.js Middleware:
const requireConditional = (req, res, next) => {
const requiresCondition = ['PUT', 'PATCH', 'DELETE'].includes(req.method)
const hasCondition = req.headers['if-match'] || req.headers['if-unmodified-since']
if (requiresCondition && !hasCondition) {
return res.status(428).json({
error: 'Precondition Required',
message: 'This operation requires a conditional header',
required_headers: ['If-Match', 'If-Unmodified-Since'],
hint: `GET ${req.path} to obtain the current ETag`
})
}
next()
}
app.put('/api/users/:id', requireConditional, async (req, res) => {
// Handle conditional update...
})
Django:
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
@require_http_methods(["PUT"])
def update_user(request, user_id):
if_match = request.headers.get('If-Match')
if not if_match:
return JsonResponse({
'error': 'Precondition Required',
'message': 'Include If-Match header with current ETag',
'required_header': 'If-Match',
'get_etag_from': f'/api/users/{user_id}'
}, status=428)
# Process conditional update
user = User.objects.get(id=user_id)
if user.etag != if_match.strip('"'):
return JsonResponse({
'error': 'Precondition Failed'
}, status=412)
# Update user...
Ruby on Rails:
class UsersController < ApplicationController
before_action :require_precondition, only: [:update, :destroy]
def update
@user = User.find(params[:id])
if stale?(etag: @user, last_modified: @user.updated_at)
if @user.update(user_params)
render json: @user
else
render json: @user.errors, status: :unprocessable_entity
end
end
end
private
def require_precondition
unless request.headers['If-Match'] || request.headers['If-Unmodified-Since']
render json: {
error: 'Precondition Required',
message: 'Include If-Match or If-Unmodified-Since header',
required_headers: ['If-Match', 'If-Unmodified-Since']
}, status: 428
end
end
end
Client-Side Handling
JavaScript Fetch API:
async function updateUser(userId, updates) {
// First, get current resource
const getResponse = await fetch(`/api/users/${userId}`)
const etag = getResponse.headers.get('ETag')
// Then update with If-Match
const putResponse = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'If-Match': etag
},
body: JSON.stringify(updates)
})
if (putResponse.status === 428) {
throw new Error('Precondition required - this should not happen!')
}
if (putResponse.status === 412) {
// Resource changed, need to refetch
console.log('Resource was modified, retrying...')
return updateUser(userId, updates) // Retry
}
return putResponse.json()
}
Try It Yourself
Visit our request builder and see 428 Precondition Required:
- Set method to PUT
- Set path to /api/protected-resource
- Click Send request (without If-Match header)
- Observe 428 response
- Add If-Match header and retry
Try it with curl
Send an update to a server that requires a conditional header, with none attached.
curl -i -X PUT https://api.example.com/documents/1 \
-H 'Content-Type: application/json' \
-d '{"title":"New title"}'
Example output (illustrative, not captured from a real server):
HTTP/2 428
content-type: application/json
{"error":"precondition_required","hint":"send If-Match"}
Fetch the current ETag with curl -I https://api.example.com/documents/1, then repeat the PUT with -H 'If-Match: "<etag>"'.
Related Status Codes
- 412 Precondition Failed - Conditional header provided but condition not met
- 409 Conflict - Request conflicts with current resource state
- 304 Not Modified - Conditional GET shows resource unchanged
- 200 OK - Successful conditional update
Frequently asked questions
What does 428 Precondition Required mean?
A 428 error means the server requires the request to include conditional headers like If-Match or If-Unmodified-Since. This prevents the lost update problem in concurrent editing scenarios.
How do I fix a 428 error?
First GET the resource to obtain its current ETag, then include If-Match header with that ETag in your PUT or PATCH request. This proves you are updating the version you intended to modify.
What is the lost update problem?
When two users edit the same resource simultaneously, one update can overwrite the other. Requiring preconditions ensures updates only succeed if the resource has not changed since it was read.
What is the difference between 428 and 412?
428 means you must include precondition headers but did not. 412 means you included precondition headers but they failed. 428 is missing headers; 412 is failed conditions.
Sources
Related
400 Bad Request
400 Bad Request means the server could not parse your request. Find which layer sent it, fix bad JSON and oversized cookies or headers, and reproduce with curl.
405 Method Not Allowed
405 Method Not Allowed means the URL exists but not for that method. Read the Allow header, then fix redirects turning POST into GET, static hosting and WAFs.
406 Not Acceptable
The server cannot produce a response matching the client's Accept headers. Learn about content negotiation and how to handle format mismatches.
408 Request Timeout
408 Request Timeout means the server gave up waiting for the client to finish sending. Tune nginx, Apache, Node and ALB timeouts and tell it apart from 504.