- Home
- HTTP Headers
- If-Match Header
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.
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()
}
Related Headers
- ETag - Resource version identifier
- If-None-Match - Conditional request for cache validation
- If-Unmodified-Since - Time-based conditional request
- If-Range - Conditional range request
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.