< HTTP/1.1 412 Precondition Failed412 Precondition Failed: Meaning and How to Fix It
412 means an If-Match or If-Unmodified-Since condition was false, usually because the resource changed after you read it. How to fix it and retry safely.
- Cacheable
- Only with explicit freshness
- Retry?
- After re-fetching current validators (ETag)
- Usually sent by
- Origin app
On this page
- What you see in your client
- What is 412 Precondition Failed?
- Common causes by stack
- Diagnose a failed precondition
- When Does This Happen?
- Example Responses
- Real-World Example
- 412 vs Other Conditional Response Codes
- Important Characteristics
- Common Mistakes
- Getting 412 Precondition Failed right
- Conditional Request Patterns
- Implementation Examples
- Try It Yourself
- Try it with curl
- Related Status Codes
TL;DR: 412 Precondition Failed means a condition in your request headers failed. Fetch the current resource, compare and merge your changes, then retry with its current ETag; replacing the ETag alone can overwrite another writer’s work.
What you see in your client
These are the client formats for a response carrying 412 and the reason phrase Precondition Failed; the URLs are placeholders. They are derived from library source, rather than captured server output. A server-supplied reason phrase can change the Python, .NET and Spring text.
- Axios:
AxiosError: Request failed with status code 412. With the default status validation, the promise rejects; inspecterror.response.status,error.response.dataanderror.response.headers. Axios constructs this message insettle. - fetch: the promise resolves when the HTTP response arrives.
response.status === 412andresponse.ok === false; check those before reading a success payload. A network failure rejects separately. MDN documents this distinction. - Python requests:
requests.exceptions.HTTPError: 412 Client Error: Precondition Failed for url: https://api.example.com/resource. This comes fromresponse.raise_for_status(), not fromrequests.get()alone. Save the body before raising if it contains useful diagnostics. Requests source. - curl
-f:curl: (22) The requested URL returned error: 412.curl -ishows headers and the error body without fail mode;--fail-with-bodykeeps the body while returning a failing exit code. curl source, option documentation. - .NET:
System.Net.Http.HttpRequestException: Response status code does not indicate success: 412 (Precondition Failed).This is the English message fromEnsureSuccessStatusCode(); inspect theHttpResponseMessagefirst when you need its body. Runtime message template. - Spring:
WebClientResponseExceptionwith message412 Precondition Failed from GET https://api.example.com/resourceforWebClient.retrieve(). RestTemplate’s default error handler usesHttpClientErrorException; its message can include the response body. These class names follow the Spring 6.2 WebClient source and HTTP client source.
What is 412 Precondition Failed?
A 412 Precondition Failed status code means one or more conditions specified in the request headers were not met by the server. A stale If-Match value rejects an update before it replaces the version another writer saved.
Use the ETag returned by the same resource and representation you read. Preserve its quotes: If-Match: "v100" is an entity tag; If-Match: v100 has the wrong syntax. RFC 9110 section 13.1.1 requires a strong comparison, so a weak tag such as W/"v100" cannot satisfy it.
Common causes by stack
- Django:
@condition(etag_func=..., last_modified_func=...)checks conditional headers and can return 412 before calling the view. Use the same validator calculation for GET and update; the decorator’s check still needs a transaction or conditional database update to cover a concurrent write. Django conditional processing. - Spring Boot / Spring MVC: a controller calls
ServletWebRequest.checkNotModified(...); despite the method name, it also checks write preconditions and can select 412. Inspect the supplied ETag and timestamp and return immediately when it handles the request. Spring API. - nginx: its not-modified response filter rejects a failed
If-MatchorIf-Unmodified-Sinceon an otherwise 200 response. Compare outgoingETagandLast-Modifiedwith the incoming condition; a 412 on a static-file GET can originate here. This response filter is separate from atomic write protection inside your application. nginx filter source. - Amazon S3:
If-None-Match: *on a conditional write fails with 412 if the key exists;If-Matchfails when the object ETag differs. Fetch metadata for the exact key before resolving the conflict. A concurrent delete can instead cause 409, with different multipart retry rules. AWS conditional writes. - Express or ASP.NET Core application code: your handler explicitly evaluates
If-Matchand returns 412. Check the version comparison and the save operation together. With EF Core, configure a concurrency token and mapDbUpdateConcurrencyExceptionto 412 for this conditional update. EF Core concurrency.
Diagnose a failed precondition
Capture the failing method, URL and conditional headers before retrying. Compare them with a fresh GET of that exact URL; using an ETag from a different resource or representation tests the wrong version. For a playlist returning 412, inspect these headers and the response body first. The status alone cannot identify a player-specific cause.
curl -sS -D /tmp/current-headers.txt -o /tmp/current-resource.json \
https://api.example.com/documents/1
grep -iE '^(HTTP/|etag:|last-modified:)' /tmp/current-headers.txt
A matching If-None-Match returns 304 for GET/HEAD, but 412 for other methods. If-Match: * asks whether a current representation exists; it does not pin a revision. Use the quoted ETag from your original read when protecting an edit. RFC 9110 preconditions.
When both If-Match and If-Unmodified-Since are present, RFC 9110 gives If-Match precedence and ignores the date condition. Dates have one-second resolution, so ETags are the better tool for edits that can occur within the same second. Preserve the user’s unsaved changes after a 412, show the current version, and merge before sending another write.
When Does This Happen?
You’ll see a 412 Precondition Failed response in these common situations:
1. ETag Mismatch (Optimistic Locking)
Client tries to update outdated version
If-Match: "old-etag" → Resource changed → 412
2. If-Unmodified-Since Check
Client requires resource unchanged since date
If-Unmodified-Since: Thu, 01 Jan 2026 00:00:00 GMT → Modified → 412
3. If-None-Match Precondition
Client wants to create only if not exists
If-None-Match: * → Resource exists → 412
4. Range Request Precondition
A stale If-Range takes a different path: the server ignores Range and sends the full representation, normally with 200. Use If-Match when you want a stale validator to stop the request with 412. RFC 9110 section 13.1.5.
Partial content request on modified file
If-Range: "etag-123" → ETag changed → Range ignored; full response
5. WebDAV Lock Token Mismatch
Client tries to modify locked resource
If: (<lock-token>) → Wrong token → 412
Example Responses
The HTTP exchanges and response bodies on this page are illustrative; fields such as current_etag are application choices.
ETag Mismatch:
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
ETag: "current-etag-xyz"
Last-Modified: Sun, 18 Jan 2026 14:30:00 GMT
{
"error": "Precondition Failed",
"message": "Resource has been modified since your last request",
"current_etag": "current-etag-xyz",
"provided_etag": "old-etag-abc",
"action": "Fetch the latest version and retry your update"
}
If-Unmodified-Since Failed:
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
Last-Modified: Sun, 18 Jan 2026 12:00:00 GMT
{
"error": "Precondition Failed",
"message": "Resource was modified after specified date",
"requested_date": "2026-01-18T10:00:00Z",
"last_modified": "2026-01-18T12:00:00Z"
}
If-None-Match Failed (Resource Exists):
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
ETag: "existing-resource-etag"
Location: /api/documents/doc-123
{
"error": "Precondition Failed",
"message": "Resource already exists",
"etag": "existing-resource-etag",
"resource_url": "/api/documents/doc-123"
}
Real-World Example
Imagine two users simultaneously editing the same document:
User A Fetches Document:
GET /api/documents/doc-456 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
ETag: "version-100"
Last-Modified: Sun, 18 Jan 2026 10:00:00 GMT
{
"id": "doc-456",
"title": "Project Proposal",
"content": "Original content",
"version": 100
}
User B Updates Document (succeeds):
PUT /api/documents/doc-456 HTTP/1.1
Host: api.example.com
If-Match: "version-100"
Content-Type: application/json
{
"title": "Project Proposal - Updated",
"content": "User B's changes"
}
HTTP/1.1 200 OK
ETag: "version-101"
User A Tries to Update (fails - outdated version):
PUT /api/documents/doc-456 HTTP/1.1
Host: api.example.com
If-Match: "version-100"
Content-Type: application/json
{
"title": "Project Proposal - My Changes",
"content": "User A's changes"
}
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
ETag: "version-101"
{
"error": "Precondition Failed",
"message": "Document has been modified by another user",
"conflict_details": {
"your_version": "version-100",
"current_version": "version-101",
"modified_by": "user-b",
"modified_at": "2026-01-18T10:05:00Z"
},
"action": "Fetch the latest version and merge your changes",
"current_document_url": "/api/documents/doc-456"
}
412 vs Other Conditional Response Codes
| Code | Meaning | Condition Type | Action Required |
|---|---|---|---|
| 412 | Precondition failed | If-Match, If-Unmodified-Since, If-None-Match on writes | Fetch latest and retry |
| 304 | Not modified | If-None-Match, If-Modified-Since | Use cached version |
| 428 | Precondition required | Missing precondition headers | Add conditional headers |
| 409 | Conflict | Business logic conflict | Resolve conflict manually |
Important Characteristics
Conditional Request Headers:
If-Match: "etag-value" ← Must match current ETag
If-None-Match: "etag-value" ← Must NOT match
If-Unmodified-Since: <date> ← Must not be modified after date
If-Modified-Since: <date> ← Must be modified after date (304)
If-Range: "etag-value" ← Mismatch ignores Range; full response
Optimistic Locking Pattern:
1. Client fetches resource with ETag: "v1"
2. Another client updates, ETag becomes "v2"
3. First client tries to update with If-Match: "v1"
4. Server returns 412 (precondition failed)
5. Client fetches latest version, merges changes, then retries
Safe Operations:
GET with If-None-Match → 304 Not Modified (OK)
PUT with If-Match → 412 Precondition Failed (prevents lost updates)
Common Mistakes
Guard updates with the version you read
PUT /api/resource/123
Content-Type: application/json
{"data": "updated"} ← No If-Match header, risky!
Use 304 for matching If-None-Match on GET or HEAD
GET /resource
If-None-Match: "current-etag" ← Matches current
HTTP/1.1 304 Not Modified
Provide a route to fetch the current version
A plain error body leaves the client to find that route itself:
HTTP/1.1 412 Precondition Failed
Content-Type: text/plain
Precondition failed.
Return useful conflict details
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
ETag: "current-version-etag"
{
"error": "Precondition Failed",
"current_etag": "current-version-etag",
"fetch_url": "/api/resource/123"
}
Getting 412 Precondition Failed right
Enforce the ETag when saving:
// Storage contract: documents(id, title, version); every write increments version.
app.put('/api/documents/:id', async (req, res) => {
const { rows: [doc] } = await db.query(
'SELECT id, title, version FROM documents WHERE id = $1', [req.params.id]
)
if (!doc) return res.sendStatus(404)
const currentETag = `"v${doc.version}"`
const condition = req.get('If-Match')
if (!condition) return res.sendStatus(428)
// This application uses version tags with no commas inside the opaque value.
const tags = condition.match(/(?:W\/)?"[^"]*"/g) ?? []
if (condition.trim() !== '*' && !tags.includes(currentETag)) {
return res.status(412).json({ error: 'precondition_failed', fetch_url: req.path })
}
const { rows: [updated] } = await db.query(
`UPDATE documents SET title = $1, version = version + 1
WHERE id = $2 AND ($3 OR ('"v' || version::text || '"') = ANY($4::text[]))
RETURNING id, title, version`,
[req.body.title, doc.id, condition.trim() === '*', tags]
)
if (!updated) return res.status(412).json({ error: 'precondition_failed' })
return res.set('ETag', `"v${updated.version}"`).json(updated)
})
The version predicate makes the comparison and write one database operation, including every tag in an If-Match list. The wildcard branch checks existence without pinning a version. The earlier SELECT supplies the payload to compare, while the UPDATE catches another writer committing after that read. Validate and authorize the submitted fields before this handler.
Provide Clear Error Messages:
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
ETag: "v25"
{
"error": "Precondition Failed",
"message": "Resource has been modified since you last fetched it",
"details": {
"your_version": 20,
"current_version": 25,
"changes_count": 5,
"last_modified_by": "alice@example.com",
"last_modified_at": "2026-01-18T14:30:00Z"
},
"actions": {
"fetch_latest": {
"method": "GET",
"url": "/api/documents/doc-123",
"description": "Fetch the current version"
},
"view_history": {
"method": "GET",
"url": "/api/documents/doc-123/history?from=20&to=25",
"description": "See what changed"
}
}
}
Include Current Resource State:
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
ETag: "current-etag"
{
"error": "Precondition Failed",
"current_state": {
"etag": "current-etag",
"version": 42,
"last_modified": "2026-01-18T14:30:00Z"
},
"your_request": {
"etag": "old-etag",
"version": 40
}
}
Conditional Request Patterns
Optimistic Locking:
// Client-side example
async function updateDocument(id, doc, etag, changes) {
// doc and etag are the original copy saved when the editor opened.
if (!etag || etag.startsWith('W/')) throw new Error('A strong ETag is required')
// 2. Apply changes
const updated = { ...doc, ...changes }
// 3. Update with If-Match
const updateResponse = await fetch(`/api/documents/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'If-Match': etag // Conditional update
},
body: JSON.stringify(updated)
})
if (updateResponse.status === 412) {
// Handle conflict
const currentResponse = await fetch(`/api/documents/${id}`)
if (!currentResponse.ok) throw new Error(`Reload failed: ${currentResponse.status}`)
const current = await currentResponse.json()
return { conflict: true, original: doc, current, changes }
}
if (!updateResponse.ok) throw new Error(`Update failed: ${updateResponse.status}`)
return updateResponse.json()
}
Pass the document and ETag saved when the editor opened, alongside the pending changes. Fetching a new baseline just before saving can hide another writer’s edit. The client returns the original copy, current copy and pending changes to its caller after a 412. The caller can present a merge instead of recursively replacing the validator and overwriting the other edit.
Prevent Duplicate Creation:
PUT /api/users/alice HTTP/1.1
If-None-Match: *
Content-Type: application/json
{"name": "Alice", "email": "alice@example.com"}
HTTP/1.1 412 Precondition Failed
Location: /api/users/alice
ETag: "existing-user-etag"
Implementation Examples
The snippets assume a validated request body and syntactically valid conditional headers. GET uses the same version tag and a stable representation; every writer increments the version.
Express.js:
// Storage contract: resources(id, title, version); every write increments version.
app.put('/api/resources/:id', async (req, res) => {
const { rows: [doc] } = await db.query(
'SELECT id, title, version FROM resources WHERE id = $1', [req.params.id]
)
if (!doc) return res.sendStatus(404)
const currentETag = `"v${doc.version}"`
const condition = req.get('If-Match')
if (!condition) return res.sendStatus(428)
// This application uses version tags with no commas inside the opaque value.
const tags = condition.match(/(?:W\/)?"[^"]*"/g) ?? []
if (condition.trim() !== '*' && !tags.includes(currentETag)) {
return res.status(412).json({ error: 'precondition_failed', fetch_url: req.path })
}
const { rows: [updated] } = await db.query(
`UPDATE resources SET title = $1, version = version + 1
WHERE id = $2 AND ($3 OR ('"v' || version::text || '"') = ANY($4::text[]))
RETURNING id, title, version`,
[req.body.title, doc.id, condition.trim() === '*', tags]
)
if (!updated) return res.status(412).json({ error: 'precondition_failed' })
return res.set('ETag', `"v${updated.version}"`).json(updated)
})
Django:
Use a database that supports select_for_update() and keep the check and save inside transaction.atomic(). Parse a JSON PUT from request.body, rather than request.POST. Django row locking, request bodies.
from django.db import transaction
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.utils.http import parse_etags
import json
@require_http_methods(["PUT"])
def update_resource(request, resource_id):
# Validate this JSON and authorize access before updating fields.
changes = json.loads(request.body)
with transaction.atomic():
try:
resource = Resource.objects.select_for_update().get(id=resource_id)
except Resource.DoesNotExist:
return JsonResponse({"error": "not_found"}, status=404)
current_etag = f'"v{resource.version}"'
condition = request.headers.get("If-Match")
if not condition:
return JsonResponse({"error": "precondition_required"}, status=428)
if condition.strip() != "*" and current_etag not in parse_etags(condition):
return JsonResponse({"error": "precondition_failed"}, status=412)
resource.title = changes["title"]
resource.version += 1
resource.save(update_fields=["title", "version"])
return JsonResponse(resource.to_dict(), headers={
"ETag": f'"v{resource.version}"'
})
ASP.NET Core:
This SQL Server pattern assumes a byte[] RowVersion property configured with [Timestamp] or .IsRowVersion(). Use the same quoted Base64 tag on GET. EF Core checks the original concurrency token during SaveChangesAsync() and reports a race as DbUpdateConcurrencyException.
// RowVersion is configured as a SQL Server rowversion concurrency token.
[HttpPut("api/resources/{id}")]
public async Task<IActionResult> UpdateResource(int id, [FromBody] ResourceDto dto)
{
var resource = await _context.Resources.FindAsync(id);
if (resource is null) return NotFound();
var currentETag = $"\"{Convert.ToBase64String(resource.RowVersion)}\"";
var conditions = Request.GetTypedHeaders().IfMatch;
if (conditions is null || conditions.Count == 0) return StatusCode(428);
var acceptsAnyVersion = conditions.Any(tag => tag.Tag.Value == "*");
if (!conditions.Any(tag => tag.Tag.Value == "*" ||
(!tag.IsWeak && tag.Tag.Value == currentETag)))
return StatusCode(412, new { error = "precondition_failed" });
resource.Update(dto); // Update editable fields only; leave RowVersion alone.
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// A wildcard only tests existence; a changed existing row is a 409 here.
var stillExists = await ex.Entries.Single().GetDatabaseValuesAsync() is not null;
return acceptsAnyVersion && stillExists
? StatusCode(409, new { error = "concurrent_update" })
: StatusCode(412, new { error = "precondition_failed" });
}
Response.Headers["ETag"] = $"\"{Convert.ToBase64String(resource.RowVersion)}\"";
return Ok(resource);
}
Try It Yourself
Visit our request builder and trigger a 412 response:
- Set method to PUT
- Set path to /api/documents/1
- Add header
If-Match: "old-etag" - Click Send request
- See 412 with current ETag and conflict details
Try it with curl
Send an update guarded by an If-Match ETag that no longer matches the current version.
curl -i -X PUT https://api.example.com/documents/1 \
-H 'If-Match: "stale-etag"' \
-H 'Content-Type: application/json' \
-d '{"title":"New title"}'
The conditional update could return:
HTTP/1.1 412 Precondition Failed
etag: "current-etag"
content-type: application/json
{"error":"precondition_failed"}
Fetch the current document and its ETag before deciding how to merge. A fresh validator authorizes a write against that version; it does not merge the submitted content for you.
Related Status Codes
- 304 Not Modified - Conditional GET succeeded (not modified)
- 428 Precondition Required - Server requires conditional headers
- 409 Conflict - Request conflicts with current state
- 200 OK - Successful conditional request
Frequently asked questions
What does 412 Precondition Failed mean?
A 412 error means a condition specified in the request headers (like If-Match or If-Unmodified-Since) was not met. The server refuses to perform the request because preconditions failed.
What causes a 412 error?
Common causes include ETag mismatch with If-Match header, resource modified since If-Unmodified-Since date, or If-None-Match condition not satisfied.
How do I fix a 412 error?
Fetch the latest resource and ETag, compare it with the version you edited, merge your changes, and send the merged update with that ETag in If-Match.
What is the difference between 412 and 409?
412 means explicit precondition headers failed. 409 means the request conflicts with resource state without using precondition headers. 412 is for conditional requests; 409 is for state conflicts.
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.