Skip to content

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:

  1. Mutation concurrencyIf-Match on PATCH/PUT/DELETE for CRUD resources. Enforced by the Resource layer using the document's revision_id.
  2. Conditional GETIf-None-Match for cacheable reads. Handled by the ETagMiddleware, which hashes response bodies and returns 304 Not Modified when 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:

ETag: "3"

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:

  1. Refetch the document to see the current state.
  2. Diff your intended change against the new state.
  3. Merge or alert the user, depending on whether the conflict is resolvable automatically.
  4. Retry the PATCH/PUT/DELETE with 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.pyETagMiddleware, generate_etag, etags_match, If-None-Match handling.
  • craft_easy/core/errors/exceptions.pyETagRequiredError (428) and ETagMismatchError (412).
  • craft_easy/settings.pyETAG_ENABLED setting.