ETag Concurrency¶
Craft Easy uses HTTP ETags for optimistic concurrency. Every document carries a revision identifier, every read returns it as an ETag header, and every mutation requires the caller to echo it back in an If-Match header. If two clients try to update the same document concurrently, one of them wins cleanly and the other sees a 412 Precondition Failed.
Two independent mechanisms live under the ETag name:
- Mutation concurrency —
If-MatchonPATCH/PUT/DELETEfor CRUD resources. Enforced by the Resource layer using the document'srevision_id. - Conditional GET —
If-None-Matchfor cacheable reads. Handled by theETagMiddleware, which hashes response bodies and returns304 Not Modifiedwhen the client already has the latest version.
Mutation concurrency¶
The Read→Modify→Write flow looks like this:
# 1. Fetch the document
RESPONSE=$(curl -sD - https://api.example.com/products/507f...)
# Extract the ETag header
ETAG=$(echo "$RESPONSE" | awk '/^ETag/ {print $2}' | tr -d '\r')
echo "$ETAG"
# "3"
# 2. Modify with If-Match
curl -X PATCH https://api.example.com/products/507f... \
-H "Content-Type: application/json" \
-H "If-Match: $ETAG" \
-d '{"price": 14.00}'
The server verifies If-Match against the document's current revision_id. Three outcomes are possible:
| Situation | Response |
|---|---|
If-Match missing on a mutating request |
428 Precondition Required — If-Match header required |
If-Match present but does not match the current revision |
412 Precondition Failed — ETag mismatch — document has been modified |
If-Match matches |
The mutation runs normally, and the response carries the new ETag. |
The errors are Craft Easy ETagRequiredError (428) and ETagMismatchError (412), both of which render as structured error envelopes — see Error Handling.
Which verbs require If-Match¶
| Verb | Requires If-Match |
|---|---|
GET |
No (but response includes ETag) |
POST |
No (new document — nothing to match) |
PATCH |
Yes |
PUT |
Yes |
DELETE |
Yes |
Bulk endpoints behave the same — each item in a bulk update must carry its own ETag.
The revision field¶
Under the hood, every document tracked by Beanie has a revision_id field that is incremented on every write. The ETag the server returns is exactly that value, wrapped in quotes:
Weak ETags (W/"...") are accepted but Craft Easy always emits strong ones. Only the bare revision is compared — prefixes and surrounding quotes are stripped.
Handling a 412¶
A 412 Precondition Failed means somebody else updated the document while you were working on it. Don't retry blindly — the right response depends on what you tried to change:
- Refetch the document to see the current state.
- Diff your intended change against the new state.
- Merge or alert the user, depending on whether the conflict is resolvable automatically.
- Retry the
PATCH/PUT/DELETEwith the new ETag.
For admin UIs, surfacing a clear "somebody else has updated this record" message is usually preferable to silent retry.
Conditional GET¶
The ETagMiddleware wraps every GET and HEAD response with a content-hash ETag so clients can skip unchanged bodies:
# First fetch — full body
curl -sD - https://api.example.com/products/507f...
# ETag: W/"a3f1..."
# Second fetch with If-None-Match — 304 Not Modified, no body
curl -sD - -H 'If-None-Match: W/"a3f1..."' https://api.example.com/products/507f...
# HTTP/1.1 304 Not Modified
These ETags are weak and hashed from the serialized body (MD5 digest). They are not the same as the revision-based ETags emitted by CRUD endpoints — they coexist. For CRUD endpoints, the revision ETag takes precedence in the response headers.
The middleware also accepts If-None-Match: *, which always matches.
Disabling ETag¶
Set ETAG_ENABLED=false to skip the middleware and the CRUD If-Match enforcement entirely. This is sometimes useful in local development or tests, but leaving it off in production exposes you to lost-update bugs.
End-to-end example¶
import httpx
client = httpx.Client(base_url="https://api.example.com", headers={"Authorization": f"Bearer {token}"})
# Read
response = client.get("/products/507f...")
product = response.json()
etag = response.headers["ETag"]
# Modify locally
product["price"] = 14.99
# Write, with the ETag we got at read time
response = client.patch(
"/products/507f...",
json=product,
headers={"If-Match": etag},
)
if response.status_code == 412:
# Somebody else updated it — refetch and retry, or surface to the user
raise ConcurrentUpdateError()
response.raise_for_status()
Source¶
craft_easy/core/crud/resource.py—_verify_etag, PATCH/PUT/DELETE handlers, ETag response header on every read.craft_easy/core/middleware/etag.py—ETagMiddleware,generate_etag,etags_match,If-None-Matchhandling.craft_easy/core/errors/exceptions.py—ETagRequiredError(428) andETagMismatchError(412).craft_easy/settings.py—ETAG_ENABLEDsetting.