# If-Match Header

> Learn how the If-Match header makes requests conditional based on ETag matching. Prevent conflicts and lost updates in concurrent editing scenarios.

Source: https://howhttpworks.com/headers/if-match
Last reviewed: 2026-10-05

> **TL;DR:** `If-Match` stops you from overwriting someone else's change. Put the ETag from your last GET in `If-Match` on your PUT, PATCH or DELETE. If the resource has changed since, the server skips the write and returns `412 Precondition Failed`; refetch, merge, and try again. On the server, check the tag and write in one atomic step, or two writers can race past it.

## What is If-Match?

`If-Match` makes a request conditional: the server only acts if the resource exists and its current ETag matches one you sent. You'll mostly use it on writes. It also works on GET when you want one specific version or an error. [RFC 9110 §13.1.1](https://www.rfc-editor.org/rfc/rfc9110#section-13.1.1) defines it.

## Syntax

Three forms (examples on this page are trimmed):

```http
If-Match: "revision-a"
If-Match: "revision-a", "revision-b"
If-Match: *
```

A list succeeds if **any** tag strongly matches. The wildcard succeeds whenever the resource currently exists, whatever its revision. Send `*` on its own, never inside a tag list.

## How If-Match Works

Say your GET returned `ETag: "revision-a"`, and another editor saves a change before your PUT goes out. Your saved tag no longer matches, so the server rejects your write.

The request, with a five-byte body:

```http
PUT /notes/current HTTP/1.1
Host: example.com
If-Match: "revision-a"
Content-Type: text/plain
Content-Length: 5

hello
```

The rejection:

```http
HTTP/1.1 412 Precondition Failed
Content-Length: 0

```

Refetch, reconcile your edit with the new version, then retry. If you just swap in the new tag and resend the old edit, you overwrite the other person's change and the check bought you nothing.

## Common Examples

`If-Match: *` stops a PUT from creating a resource that doesn't exist yet. A specific tag on DELETE makes sure you delete the version you looked at. On a GET with Range, a failed If-Match gets you a 412 rather than the new full representation; use [If-Range](https://howhttpworks.com/headers/if-range) when you'd rather receive the full response.

An editor should hold on to the ETag of the version the user started editing. This browser example loads a text note, then saves the user's edited text with that ETag:

```javascript
async function openNote(url) {
  const response = await fetch(url, { cache: 'no-store' });
  if (!response.ok) throw new Error(`Read failed: ${response.status}`);
  const etag = response.headers.get('etag');
  if (!etag || etag.startsWith('W/')) {
    throw new Error('This editor requires a strong ETag');
  }
  return { url, etag, original: await response.text() };
}

async function saveNote(note, editedText) {
  const response = await fetch(note.url, {
    method: 'PUT',
    headers: { 'If-Match': note.etag, 'Content-Type': 'text/plain' },
    body: editedText,
  });
  if (response.status === 412) {
    const latest = await openNote(note.url);
    return { conflict: true, original: note.original, editedText, latest };
  }
  if (!response.ok) throw new Error(`Save failed: ${response.status}`);
  return { conflict: false, saved: await openNote(note.url) };
}
```

On a conflict, show all three versions: the original, the user's edit, and the latest one. That way nobody loses work and the user can merge. Resist fetching a fresh ETag right before each save. It only protects the split second after that read and misses every change made while the user was typing. For a cross-origin API, expose `ETag` through CORS so the first function can read it.

## Server Implementation

Run your normal request checks first, then evaluate the precondition, then apply the method. [RFC 9110 §13.2.1](https://www.rfc-editor.org/rfc/rfc9110#section-13.2.1) says to ignore preconditions when the same request without them would have produced something other than 2xx or 412, so an authorization error or a redirect wins over a 412.

Do the version comparison and the write in one transaction or one conditional database operation. If your code checks the tag, then awaits a separate write, another writer can slip in between and you lose an update anyway.

Parse the header properly. A substring test like `header.includes(currentETag)` matches the current tag inside `W/"revision-a"`, and weak tags must never match here. Splitting on commas isn't enough either, because the quoted tag itself can contain a comma. A strong match needs two non-weak tags with identical opaque values.

Here's a Flask demo for a single text resource that always exists. Save it as `match_demo.py` somewhere Flask is installed and run `flask --app match_demo run`. The PUT path uses Werkzeug's [parsed ETags](https://werkzeug.palletsprojects.com/en/stable/datastructures/#werkzeug.datastructures.ETags.contains) to do a strong comparison instead of searching the raw header:

```python
from hashlib import sha256
from threading import Lock
from flask import Flask, Response, request

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024
lock = Lock()
body = b'hello'

def reply(data):
    response = Response(data, content_type='text/plain')
    response.set_etag(sha256(data).hexdigest())
    response.headers['Cache-Control'] = 'no-store'
    return response

@app.route('/note', methods=['GET', 'PUT'])
def note():
    global body
    if request.method in ('GET', 'HEAD'):
        with lock:
            return reply(body)
    if request.content_type != 'text/plain':
        return Response(status=415)
    if 'If-Match' not in request.headers:
        return Response('Send If-Match for this endpoint', status=428)
    replacement = request.get_data()
    with lock:
        current = sha256(body).hexdigest()
        if not request.if_match.contains(current):
            return Response(status=412)
        body = replacement
        return reply(body)
```

The 1 MiB body limit is just an example setting. The [Flask API](https://flask.palletsprojects.com/en/stable/api/) provides `request.if_match` and the response's `set_etag`, which adds the quotes around the hash for you. `If-Match: *` matches here because the resource always exists, and a weak tag with the same hash doesn't.

The demo keeps the check and write under one in-process lock and returns exactly the bytes the PUT stored. Its state lives in memory, so it resets on restart and isn't shared between server processes. In a real application, put both steps inside the database transaction that owns the resource, and make every code path that modifies it follow the same rule. Returning [428 Precondition Required](https://www.rfc-editor.org/rfc/rfc6585#section-3) when If-Match is missing is a choice this endpoint makes; supporting If-Match doesn't require it.

## Getting If-Match right

When the condition fails, the server must not apply the method. The usual response is 412. The RFC also lets the server return a 2xx if it can tell the requested change has already been made, for example a retried PUT that already succeeded. A 412 doesn't always mean someone else edited the resource; it may have been deleted, or the server may have selected a different representation.

If a request carries both If-Match and [If-Unmodified-Since](https://howhttpworks.com/headers/if-unmodified-since), the server ignores If-Unmodified-Since. You don't get two checks stacked on top of each other.

A strong ETag belongs to one specific representation. If your API returns different bytes for JSON and another media type, or for compressed and uncompressed responses, each needs its own strong validator. Capture the ETag from the exact representation the user is editing, and have the server compare against that same representation. An application revision number works as an ETag only if it meets the strong-validator rules for what you actually serve.

Successful PUTs come with a validator rule too. [RFC 9110 §9.3.4](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.4) forbids sending a validator in a successful PUT response unless the server saved the submitted representation without transforming it, and the validator reflects that saved version. If your API parses submitted JSON and stores or returns a normalized form, leave the ETag out of the PUT response and have the client refetch to get it.

A rejected write must leave both the content and the revision untouched. Check both in your concurrency tests; a 412 sent after the update already committed protects nothing.

## If-Match vs If-None-Match

If-Match needs a match and uses **strong** comparison. [If-None-Match](https://howhttpworks.com/headers/if-none-match) needs no match and uses **weak** comparison. On a PUT, `If-None-Match: *` means "create only", the mirror image of `If-Match: *`, which means "update only".

## Testing If-Match

Pick a throwaway resource on an API that supports conditional writes. Copy its real ETag, quotes included, into the PUT:

```bash
curl -sS -D - https://example.com/notes/current
curl -i -X PUT https://example.com/notes/current \
  -H 'If-Match: "revision-a"' \
  -H 'Content-Type: text/plain' --data-binary 'hello'
```

Once that succeeds, send another PUT with the old tag and a **different** body. You should get 412, and the resource should be unchanged. Also try the weak form of the current tag (`W/"..."`); it should fail the strong comparison.

Against the local Flask route, read the current tag, then send two different replacements using it:

```bash
curl -sS -D /tmp/note.headers http://127.0.0.1:5000/note
```

Copy the quoted ETag from that response into the following commands:

```bash
curl -i -X PUT http://127.0.0.1:5000/note \
  -H 'If-Match: "copied-tag"' -H 'Content-Type: text/plain' \
  --data-binary 'first edit'
curl -i -X PUT http://127.0.0.1:5000/note \
  -H 'If-Match: "copied-tag"' -H 'Content-Type: text/plain' \
  --data-binary 'second edit'
```

The first PUT succeeds and the note gets a new tag. The second should fail with 412, leaving `first edit` in place. Then leave out If-Match to see this route's 428, and try `If-Match: *` to see how much weaker an existence check is than a version check.

## Related Headers

- [ETag](https://howhttpworks.com/headers/etag)
- [If-None-Match](https://howhttpworks.com/headers/if-none-match)
- [If-Unmodified-Since](https://howhttpworks.com/headers/if-unmodified-since)
- [If-Range](https://howhttpworks.com/headers/if-range)
