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

Source: https://howhttpworks.com/status-codes/412
Last reviewed: 2026-10-05

> **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`](https://github.com/axios/axios/blob/v1.x/lib/core/settle.js).
- **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](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch).
- **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](https://requests.readthedocs.io/en/latest/_modules/requests/models/#Response.raise_for_status).
- **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](https://github.com/curl/curl/blob/master/lib/http.c), [option documentation](https://curl.se/docs/manpage.html#--fail-with-body).
- **.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](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/Resources/Strings.resx).
- **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](https://github.com/spring-projects/spring-framework/blob/6.2.x/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java) and [HTTP client source](https://github.com/spring-projects/spring-framework/blob/v6.2.0/spring-web/src/main/java/org/springframework/web/client/HttpClientErrorException.java).

## 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](https://www.rfc-editor.org/rfc/rfc9110#name-if-match) 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](https://docs.djangoproject.com/en/5.2/topics/conditional-view-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](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/context/request/ServletWebRequest.html).
- **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](https://github.com/nginx/nginx/blob/master/src/http/modules/ngx_http_not_modified_filter_module.c).
- **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](https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-writes.html).
- **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](https://learn.microsoft.com/en-us/ef/core/saving/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.

```bash
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](https://howhttpworks.com/status-codes/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](https://www.rfc-editor.org/rfc/rfc9110#name-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)**

```text
Client tries to update outdated version
If-Match: "old-etag" → Resource changed → 412
```

**2. If-Unmodified-Since Check**

```text
Client requires resource unchanged since date
If-Unmodified-Since: Thu, 01 Jan 2026 00:00:00 GMT → Modified → 412
```

**3. If-None-Match Precondition**

```text
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](https://www.rfc-editor.org/rfc/rfc9110#name-if-range).

```text
Partial content request on modified file
If-Range: "etag-123" → ETag changed → Range ignored; full response
```

**5. WebDAV Lock Token Mismatch**

```text
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
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
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
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:**

```http
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):**

```http
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):**

```http
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:**

```text
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:**

```text
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:**

```text
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**

```text
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**

```text
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
HTTP/1.1 412 Precondition Failed
Content-Type: text/plain

Precondition failed.
```

**Return useful conflict details**

```http
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:**

```javascript
// 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
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
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:**

```javascript
// 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:**

```http
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:**

```javascript
// 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](https://docs.djangoproject.com/en/5.2/ref/models/querysets/#select-for-update), [request bodies](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest.body).

```python
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`.

```csharp
// 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](https://howhttpworks.com/tools/playground) 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.

```bash
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
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](https://howhttpworks.com/status-codes/304) - Conditional GET succeeded (not modified)
- [428 Precondition Required](https://howhttpworks.com/status-codes/428) - Server requires conditional headers
- [409 Conflict](https://howhttpworks.com/status-codes/409) - Request conflicts with current state
- [200 OK](https://howhttpworks.com/status-codes/200) - Successful conditional request
