How HTTP Works
4xx · Client error
< HTTP/1.1 412 Precondition Failed

412 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.

Reviewed 12 min readintermediate2 sourcesTry itMarkdown
Cacheable
Only with explicit freshness
Retry?
After re-fetching current validators (ETag)
Usually sent by
Origin app
Spec
RFC 9110 §15.5.13
Often confused with
409, 428
On this page

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; inspect error.response.status, error.response.data and error.response.headers. Axios constructs this message in settle.
  • fetch: the promise resolves when the HTTP response arrives. response.status === 412 and response.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 from response.raise_for_status(), not from requests.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 -i shows headers and the error body without fail mode; --fail-with-body keeps 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 from EnsureSuccessStatusCode(); inspect the HttpResponseMessage first when you need its body. Runtime message template.
  • Spring: WebClientResponseException with message 412 Precondition Failed from GET https://api.example.com/resource for WebClient.retrieve(). RestTemplate’s default error handler uses HttpClientErrorException; 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-Match or If-Unmodified-Since on an otherwise 200 response. Compare outgoing ETag and Last-Modified with 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-Match fails 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-Match and returns 412. Check the version comparison and the save operation together. With EF Core, configure a concurrency token and map DbUpdateConcurrencyException to 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

CodeMeaningCondition TypeAction Required
412Precondition failedIf-Match, If-Unmodified-Since, If-None-Match on writesFetch latest and retry
304Not modifiedIf-None-Match, If-Modified-SinceUse cached version
428Precondition requiredMissing precondition headersAdd conditional headers
409ConflictBusiness logic conflictResolve 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:

  1. Set method to PUT
  2. Set path to /api/documents/1
  3. Add header If-Match: "old-etag"
  4. Click Send request
  5. 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.

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

  1. MDN Web Docs: 412 Precondition Faileddeveloper.mozilla.org
  2. RFC 9110: 412 Precondition Failedrfc-editor.org

Keep going

Browse /search