Skip to main content

Guard an HTTP update with strong If-Match

2026-11-13 14:12:17.767823+00:00
Guard an HTTP update with strong If-Match

AI assisted the preparation of this article, and its Python example was tested locally with synthetic fixtures.

Two editors can read the same record and save conflicting changes. An API that supports conditional writes can reject the second update when its strong entity tag no longer matches the current representation.

Flow: Read tag, then Edit record, then Atomic check, then Save or 412.

Compare and update together

This disposable server fixture stores one synthetic byte string and one integer version in memory. It accepts exactly one quoted strong tag such as "v1". A missing tag returns 428 by fixture policy; a weak or stale tag returns 412. The /fixture/error route simulates a server error.

from http.server import BaseHTTPRequestHandler
from threading import Lock

class Store:
    def __init__(self):
        self.lock = Lock()
        self.version = 1
        self.body = b"original"
    def read(self):
        with self.lock:
            return self.body, f'"v{self.version}"'
    def update(self, tag, body):
        with self.lock:
            current = f'"v{self.version}"'
            if tag != current:
                return False
            self.body = body
            self.version += 1
            return True

class Handler(BaseHTTPRequestHandler):
    store = None
    def log_message(self, *args):
        pass
    def do_GET(self):
        body, tag = self.store.read()
        self.send_response(200)
        self.send_header("ETag", tag)
        self.end_headers()
        self.wfile.write(body)
    def do_PUT(self):
        length = int(self.headers.get("Content-Length", "0"))
        body = self.rfile.read(length)
        if self.path.endswith("/error"):
            self.send_response(500)
            self.end_headers()
            return
        tag = self.headers.get("If-Match")
        if tag is None:
            self.send_response(428)
            self.end_headers()
            return
        status = 200 if self.store.update(tag, body) else 412
        self.send_response(status)
        self.end_headers()

The lock encloses both the comparison and the update. Two clients can read version one; after the first update changes it to version two, the second version-one update receives 412 and cannot replace the first body. A precondition checked outside the lock would leave a race in this fixture.

RFC 9110 If-Match uses strong entity-tag comparison and explains conditional writes. The RFC allows more tag syntax than this exact-string toy policy, including lists and *; this handler intentionally does not implement the full grammar. The 428 response is a chosen missing-header policy, not a universal requirement.

Test the actual boundary

The local test starts a threaded HTTP server on an ephemeral loopback port, reads the tag, sends successful and stale writes, checks weak and missing tags, and confirms a simulated 500 leaves the body unchanged. A finally block shuts down and joins the server. All data is synthetic; no provider is contacted.

A real service must implement and atomically enforce its own precondition using its storage system. A header sent to a server that ignores it does not prevent a lost update. Authentication, body-size limits, full HTTP parsing, conflict resolution and a timeout after an uncertain write need separate handling. This example is not a production server, cache or proof that another API supports compare-and-swap.