HTTP

Header

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.

7 min read advanced Try in Playground

TL;DR: Prevents lost updates by making PUT/PATCH/DELETE requests conditional on matching ETags. Server returns 412 Precondition Failed if the resource was modified by someone else.

What is If-Match?

The If-Match header makes a request conditional by specifying that the server should only process the request if the resource’s current ETag matches the provided value. It’s like saying “only do this operation if the resource is still exactly what I think it is.”

This prevents the “lost update problem” where concurrent modifications overwrite each other.

How If-Match Works

Client fetches resource:

GET /api/posts/123 HTTP/1.1
Host: api.example.com

HTTP/1.1 200 OK
ETag: "abc123"
Content-Type: application/json

{"id": 123, "title": "My Post", "content": "..."}
```text

**Client updates with If-Match:**

```http
PUT /api/posts/123 HTTP/1.1
Host: api.example.com
If-Match: "abc123"
Content-Type: application/json

{"id": 123, "title": "Updated Post", "content": "..."}

If ETag matches, update succeeds:

HTTP/1.1 200 OK
ETag: "def456"

{"id": 123, "title": "Updated Post"}
```text

**If ETag doesn't match, update fails:**

```http
HTTP/1.1 412 Precondition Failed

{"error": "Resource has been modified by another user"}

Syntax

If-Match: "<etag>"
If-Match: "<etag>", "<etag>", ...
If-Match: *
```text

### Values

- **"etag"** - Specific ETag value (must be quoted)
- **\*** - Match any existing resource (if-exists check)
- Multiple ETags separated by commas

## Common Examples

### Conditional Update

```http
If-Match: "abc123"

Only update if ETag is “abc123”.

Multiple ETags

If-Match: "abc123", "def456"
```text

Update if ETag is either "abc123" or "def456".

### If-Exists Check

```http
If-Match: *

Only proceed if the resource exists (any ETag).

Weak ETag

If-Match: W/"abc123"
```text

Weak ETags always fail If-Match (only strong ETags work).

## Real-World Scenarios

### Preventing Lost Updates

```http
# User A fetches the post
GET /api/posts/123 HTTP/1.1

HTTP/1.1 200 OK
ETag: "v1"
{"title": "Original Title"}

# User A updates with If-Match
PUT /api/posts/123 HTTP/1.1
If-Match: "v1"
{"title": "User A's Title"}

# Meanwhile, User B already updated it
HTTP/1.1 412 Precondition Failed
{"error": "Post was modified. Current ETag: v2"}

# User A must fetch latest version and retry
GET /api/posts/123 HTTP/1.1

HTTP/1.1 200 OK
ETag: "v2"
{"title": "User B's Title"}

PUT /api/posts/123 HTTP/1.1
If-Match: "v2"
{"title": "User A's Title"}

HTTP/1.1 200 OK
ETag: "v3"

Atomic Delete

# Ensure we're deleting the exact version we want
DELETE /api/documents/456 HTTP/1.1
If-Match: "doc-version-5"

HTTP/1.1 204 No Content
```text

### Conditional Partial Update

```http
# PATCH with optimistic locking
PATCH /api/users/789 HTTP/1.1
If-Match: "user-abc123"
Content-Type: application/json

{"email": "newemail@example.com"}

HTTP/1.1 200 OK
ETag: "user-def456"

File Upload

# Replace file only if current version matches
PUT /files/document.pdf HTTP/1.1
If-Match: "file-v3"
Content-Type: application/pdf

[PDF data]

HTTP/1.1 200 OK
ETag: "file-v4"
```javascript

## Server Implementation

### Express.js (Node.js)

```javascript
const express = require('express')
const crypto = require('crypto')
const app = express()

// Generate ETag from data
function generateETag(data) {
  return crypto.createHash('md5').update(JSON.stringify(data)).digest('hex')
}

// Mock database
const posts = {
  123: {
    id: 123,
    title: 'My Post',
    content: 'Content here',
    version: 1
  }
}

// GET with ETag
app.get('/api/posts/:id', (req, res) => {
  const post = posts[req.params.id]
  if (!post) {
    return res.status(404).json({ error: 'Not found' })
  }

  const etag = `"${generateETag(post)}"`
  res.setHeader('ETag', etag)
  res.json(post)
})

// PUT with If-Match
app.put('/api/posts/:id', express.json(), (req, res) => {
  const post = posts[req.params.id]
  if (!post) {
    return res.status(404).json({ error: 'Not found' })
  }

  const ifMatch = req.headers['if-match']

  if (ifMatch) {
    const currentETag = `"${generateETag(post)}"`

    // Check for wildcard
    if (ifMatch === '*') {
      // Resource exists, continue
    }
    // Check for matching ETags
    else if (!ifMatch.includes(currentETag)) {
      return res.status(412).json({
        error: 'Precondition Failed',
        message: 'Resource has been modified',
        currentETag: currentETag
      })
    }
  }

  // Update the post
  Object.assign(post, req.body)
  post.version++

  const newETag = `"${generateETag(post)}"`
  res.setHeader('ETag', newETag)
  res.json(post)
})

// DELETE with If-Match
app.delete('/api/posts/:id', (req, res) => {
  const post = posts[req.params.id]
  if (!post) {
    return res.status(404).json({ error: 'Not found' })
  }

  const ifMatch = req.headers['if-match']

  if (ifMatch && ifMatch !== '*') {
    const currentETag = `"${generateETag(post)}"`

    if (!ifMatch.includes(currentETag)) {
      return res.status(412).json({
        error: 'Precondition Failed',
        currentETag: currentETag
      })
    }
  }

  delete posts[req.params.id]
  res.status(204).end()
})

Middleware for If-Match

function checkIfMatch(req, res, next) {
  const ifMatch = req.headers['if-match']
  if (!ifMatch) {
    return next()
  }

  const resource = getResource(req.params.id)
  if (!resource) {
    return res.status(404).json({ error: 'Not found' })
  }

  const currentETag = `"${generateETag(resource)}"`

  // Wildcard always matches existing resources
  if (ifMatch === '*') {
    req.resource = resource
    return next()
  }

  // Parse multiple ETags
  const requestedETags = ifMatch.split(',').map((e) => e.trim())

  // Check if any ETag matches
  if (!requestedETags.includes(currentETag)) {
    return res.status(412).json({
      error: 'Precondition Failed',
      currentETag: currentETag
    })
  }

  req.resource = resource
  next()
}

app.put('/api/posts/:id', checkIfMatch, (req, res) => {
  // Resource is validated, proceed with update
  updateResource(req.resource, req.body)
  res.json(req.resource)
})
```javascript

### FastAPI (Python)

```python
from fastapi import FastAPI, Request, HTTPException, Response
from hashlib import md5
import json

app = FastAPI()

# Mock database
posts = {
    123: {
        "id": 123,
        "title": "My Post",
        "content": "Content here",
        "version": 1
    }
}

def generate_etag(data):
    content = json.dumps(data, sort_keys=True)
    return f'"{md5(content.encode()).hexdigest()}"'

@app.get("/api/posts/{post_id}")
async def get_post(post_id: int, response: Response):
    post = posts.get(post_id)
    if not post:
        raise HTTPException(status_code=404, detail="Not found")

    etag = generate_etag(post)
    response.headers["ETag"] = etag

    return post

@app.put("/api/posts/{post_id}")
async def update_post(post_id: int, data: dict, request: Request, response: Response):
    post = posts.get(post_id)
    if not post:
        raise HTTPException(status_code=404, detail="Not found")

    if_match = request.headers.get("if-match")

    if if_match:
        current_etag = generate_etag(post)

        # Check wildcard
        if if_match == "*":
            pass  # Resource exists, continue
        # Check matching ETags
        elif current_etag not in if_match:
            raise HTTPException(
                status_code=412,
                detail={
                    "error": "Precondition Failed",
                    "currentETag": current_etag
                }
            )

    # Update post
    post.update(data)
    post["version"] += 1

    new_etag = generate_etag(post)
    response.headers["ETag"] = new_etag

    return post

@app.delete("/api/posts/{post_id}")
async def delete_post(post_id: int, request: Request):
    post = posts.get(post_id)
    if not post:
        raise HTTPException(status_code=404, detail="Not found")

    if_match = request.headers.get("if-match")

    if if_match and if_match != "*":
        current_etag = generate_etag(post)

        if current_etag not in if_match:
            raise HTTPException(
                status_code=412,
                detail={"error": "Precondition Failed"}
            )

    del posts[post_id]
    return Response(status_code=204)

Django

from django.http import JsonResponse, HttpResponse
from django.views.decorators.http import require_http_methods
import hashlib
import json

def generate_etag(data):
    content = json.dumps(data, sort_keys=True)
    return f'"{hashlib.md5(content.encode()).hexdigest()}"'

@require_http_methods(["PUT"])
def update_post(request, post_id):
    post = get_post_from_db(post_id)
    if not post:
        return JsonResponse({'error': 'Not found'}, status=404)

    if_match = request.META.get('HTTP_IF_MATCH')

    if if_match:
        current_etag = generate_etag(post)

        if if_match == '*':
            pass  # Exists, continue
        elif current_etag not in if_match:
            return JsonResponse({
                'error': 'Precondition Failed',
                'currentETag': current_etag
            }, status=412)

    # Update post
    data = json.loads(request.body)
    post.update(data)
    save_post(post)

    new_etag = generate_etag(post)
    response = JsonResponse(post)
    response['ETag'] = new_etag

    return response
```text

## Best Practices

### For Servers

**1. Always check If-Match for updates**

```javascript
// ✅ Implement optimistic locking
if (req.headers['if-match']) {
  if (!matchesCurrentETag()) {
    return res.status(412).send('Precondition Failed')
  }
}

2. Return current ETag on 412

# ✅ Help client recover
HTTP/1.1 412 Precondition Failed
ETag: "current-value"

{"error": "Resource modified", "currentETag": "current-value"}
```text

**3. Require If-Match for critical operations**

```javascript
// ✅ Require If-Match for deletes
app.delete('/api/posts/:id', (req, res) => {
  if (!req.headers['if-match']) {
    return res.status(428).json({
      error: 'If-Match header required'
    })
  }
  // Process delete
})

4. Support wildcard for existence checks

if (ifMatch === '*') {
  // Just check if resource exists
  if (!resourceExists) {
    return res.status(412).send()
  }
}
```javascript

**5. Only use strong ETags with If-Match**

```javascript
// ✅ Generate strong ETags
const etag = `"${hash(resource)}"`

// ❌ Weak ETags don't work with If-Match
const etag = `W/"${hash(resource)}"`

For Clients

1. Always use If-Match for updates

// ✅ Fetch ETag, then update
const response = await fetch('/api/post/123')
const post = await response.json()
const etag = response.headers.get('ETag')

await fetch('/api/post/123', {
  method: 'PUT',
  headers: {
    'If-Match': etag,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ title: 'New Title' })
})
```javascript

**2. Handle 412 responses**

```javascript
try {
  const response = await fetch('/api/post/123', {
    method: 'PUT',
    headers: { 'If-Match': etag },
    body: JSON.stringify(updates)
  })

  if (response.status === 412) {
    // Refetch and retry
    console.log('Resource was modified, refetching...')
    const latest = await fetch('/api/post/123')
    // Show conflict resolution UI
  }
} catch (error) {
  console.error('Update failed:', error)
}

3. Store ETags with data

// ✅ Keep ETag with cached data
const cache = {
  data: post,
  etag: response.headers.get('ETag')
}

// Use stored ETag for updates
fetch('/api/post/123', {
  method: 'PUT',
  headers: { 'If-Match': cache.etag },
  body: JSON.stringify(updates)
})
```text

## If-Match vs If-None-Match

### If-Match (Update Safety)

```http
# "Only update if it's still version X"
PUT /api/post/123
If-Match: "abc123"

# 200 OK = Updated
# 412 Precondition Failed = Someone else modified it

If-None-Match (Cache Validation)

# "Only send if it's NOT version X"
GET /api/post/123
If-None-Match: "abc123"

# 200 OK = New content
# 304 Not Modified = Still same version
```text

## Common Use Cases

### Optimistic Locking

```http
If-Match: "version-5"

Prevent lost updates in concurrent editing.

Atomic Operations

If-Match: "state-abc"
```text

Ensure state hasn't changed before operation.

### Safe Deletions

```http
DELETE /resource
If-Match: "specific-version"

Only delete exact version intended.

Conditional Creation Prevention

PUT /resource
If-Match: *
```text

Only update if resource already exists.

## Testing If-Match

### Using curl

```bash
# Fetch ETag
curl -i https://api.example.com/posts/123

# Update with If-Match
curl -X PUT https://api.example.com/posts/123 \
  -H "If-Match: \"abc123\"" \
  -H "Content-Type: application/json" \
  -d '{"title": "New Title"}'

# Test mismatch (should get 412)
curl -X PUT https://api.example.com/posts/123 \
  -H "If-Match: \"wrong-etag\"" \
  -d '{"title": "New Title"}'

Using JavaScript

// Complete update flow
async function updateWithOptimisticLocking(id, updates) {
  // Fetch current version
  const getResponse = await fetch(`/api/posts/${id}`)
  const currentData = await getResponse.json()
  const etag = getResponse.headers.get('ETag')

  // Update with If-Match
  const updateResponse = await fetch(`/api/posts/${id}`, {
    method: 'PUT',
    headers: {
      'If-Match': etag,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(updates)
  })

  if (updateResponse.status === 412) {
    throw new Error('Resource was modified by another user')
  }

  return updateResponse.json()
}

Frequently Asked Questions

What is If-Match?

If-Match is a precondition header for PUT/PATCH/DELETE requests. The server only processes the request if the current ETag matches, preventing overwrites of concurrent changes.

How does If-Match prevent lost updates?

When updating, send the ETag you read. If someone else modified the resource, ETags wont match and the server returns 412 Precondition Failed instead of overwriting.

What does If-Match: * mean?

If-Match: * matches any existing resource. Use it to ensure the resource exists before updating, without caring about the specific version.

When should I use If-Match vs If-Unmodified-Since?

If-Match with ETags is more precise. If-Unmodified-Since uses timestamps with second precision. Use If-Match for critical updates where precision matters.

Keep Learning