DEV Community

Dakota Liu
Dakota Liu

Posted on

Case Study: Freeze If-Match Version Tokens Before an Agent Writes the PATCH Client

You should freeze optimistic concurrency before an agent writes any PATCH client, because agents default to last-write-wins and quietly drop concurrent edits. A small notes API makes the failure cheap to reproduce: two writers, one version column, and a contract test that rejects a missing or stale If-Match. Once that contract is red on purpose, you can let an agent fill in handlers without inventing timestamps, updated_at comparisons, or silent overwrites. This case study walks one project from background through tests, implementation, results, and the limits of the pattern.

Background: a notes resource two people can edit

The project is a single-resource notes service with GET /notes/:id and PATCH /notes/:id. You store id, body, and an integer version that only the server increments. The product rule is simple and non-negotiable: a write with a stale version must fail with 412 Precondition Failed, and a write with no version token must fail with 428 Precondition Required. Agents rarely infer that pair from a prose ticket, so they ship last-write-wins JSON and call the ticket done.

You are not building a CRDT, an append-only event log, or a view-counter. Those domains want different merge rules, and freezing If-Match on them would be the wrong contract. You are building a document that two humans edit in the same minute, which is the case where lost updates actually hurt. Keep the surface tiny so the frozen tests stay readable and the agent cannot wander into unrelated endpoints.

Goal for this case study

The goal is not a clever client. The goal is a red test suite that already encodes the version window, then an implementation that is not allowed to change the suite. You pin four behaviors before any model writes production code:

  1. GET returns the current body plus a strong ETag derived from version.
  2. PATCH with a matching If-Match succeeds, increments version, and returns the new ETag.
  3. PATCH with a stale If-Match returns 412 and leaves body unchanged.
  4. PATCH without If-Match returns 428 and does not create a default version of 1.

If you skip step four, agents “helpfully” treat a missing header as “just write it,” which reintroduces lost updates for every retrying script. Freeze the 428 so the client is forced to GET first. That is the entire product decision; everything else is wiring.

The contract you freeze first

Save the suite under tests/test_note_if_match.py and run it against an empty app so every assertion fails for the right reason. Label this as a worked example you can copy, not a production dump from an unnamed company.

# tests/test_note_if_match.py
# Worked example: freeze optimistic concurrency before any PATCH client exists.
import json
from pathlib import Path

import pytest
from httpx import Client

BASE = "http://127.0.0.1:8088"
NOTE_ID = "note_42"


def etag_of(resp):
    value = resp.headers.get("etag")
    assert value, "GET/PATCH must send ETag"
    assert value[0] == '"' and value[-1] == '"', "use a strong ETag, not W/"
    return value


@pytest.fixture(scope="module")
def http():
    with Client(base_url=BASE, timeout=2.0) as client:
        yield client


def test_get_returns_strong_etag(http: Client):
    resp = http.get(f"/notes/{NOTE_ID}")
    assert resp.status_code == 200
    body = resp.json()
    assert body["id"] == NOTE_ID
    assert "body" in body and "version" in body
    assert etag_of(resp) == f'"{body["version"]}"'


def test_patch_requires_if_match(http: Client):
    resp = http.patch(
        f"/notes/{NOTE_ID}",
        json={"body": "agent invented a write without a token"},
    )
    assert resp.status_code == 428
    assert http.get(f"/notes/{NOTE_ID}").json()["body"] != (
        "agent invented a write without a token"
    )


def test_stale_if_match_returns_412_and_preserves_body(http: Client):
    current = http.get(f"/notes/{NOTE_ID}")
    original = current.json()["body"]
    stale = '"0"'
    resp = http.patch(
        f"/notes/{NOTE_ID}",
        headers={"If-Match": stale},
        json={"body": "should not land"},
    )
    assert resp.status_code == 412
    assert http.get(f"/notes/{NOTE_ID}").json()["body"] == original


def test_matching_if_match_bumps_version(http: Client):
    current = http.get(f"/notes/{NOTE_ID}")
    token = etag_of(current)
    version = current.json()["version"]
    resp = http.patch(
        f"/notes/{NOTE_ID}",
        headers={"If-Match": token},
        json={"body": "second writer won the race fairly"},
    )
    assert resp.status_code == 200
    assert resp.json()["version"] == version + 1
    assert etag_of(resp) == f'"{version + 1}"'


def test_second_writer_with_old_token_loses(http: Client):
    first = http.get(f"/notes/{NOTE_ID}")
    token = etag_of(first)
    ok = http.patch(
        f"/notes/{NOTE_ID}",
        headers={"If-Match": token},
        json={"body": "writer A"},
    )
    assert ok.status_code == 200
    lost = http.patch(
        f"/notes/{NOTE_ID}",
        headers={"If-Match": token},
        json={"body": "writer B should 412"},
    )
    assert lost.status_code == 412
    assert http.get(f"/notes/{NOTE_ID}").json()["body"] == "writer A"
Enter fullscreen mode Exit fullscreen mode

Run the suite before any handler exists so you watch the intended failures, not a green suite that never executed the contract:

python -m pytest tests/test_note_if_match.py -q
# expected: connection errors or 404s, not a silent pass
Enter fullscreen mode Exit fullscreen mode

What agents invent when the contract is missing

Give the same ticket to an unconstrained coding agent and you will usually get one of these substitutes. None of them preserve the two-writer rule you actually wanted.

  • Compare updated_at timestamps, which collide inside the same second and depend on clock skew.
  • Hash the body and treat equal hashes as “no write,” which hides concurrent edits that happened to agree.
  • Retry the same PATCH on 500, which applies the write twice if the first attempt actually succeeded.
  • Accept a missing If-Match and default version to 1, which turns the first retry client into last-write-wins.
  • Emit a weak ETag (W/"3") and then compare it with strong equality in the handler.

A decision table belongs in the repo next to the tests, because prose in a chat window is not a contract. Keep it short enough that a reviewer can reject a pull request against one row.

Incoming If-Match Current version Status Body mutated? Next ETag
missing any 428 no unchanged
"3" 3 200 yes "4"
"2" 3 412 no "3"
W/"3" 3 412 no "3"
* 3 412 no "3"

You freeze * as rejected on purpose. Wildcard success is a last-write-wins escape hatch, and agents love escape hatches. If you later need an admin overwrite, add a separate authenticated route instead of loosening PATCH.

Implementation after the suite is red

Only after the tests exist do you allow an agent to write app.py. Seed one note so GET is deterministic, store versions in process memory for the case study, and refuse every write that does not present a strong token. The snippet below is labeled example code for local reproduction, not a claim about a shipped service.

# app.py — example server that satisfies tests/test_note_if_match.py
from fastapi import FastAPI, Header, HTTPException, Response
from pydantic import BaseModel

app = FastAPI()
NOTES = {"note_42": {"id": "note_42", "body": "seed", "version": 1}}

class Patch(BaseModel):
    body: str

def strong_etag(version: int) -> str:
    return f'"{version}"'

@app.get("/notes/{note_id}")
def get_note(note_id: str, response: Response):
    note = NOTES.get(note_id)
    if note is None:
        raise HTTPException(status_code=404)
    response.headers["ETag"] = strong_etag(note["version"])
    return note

@app.patch("/notes/{note_id}")
def patch_note(
    note_id: str,
    payload: Patch,
    response: Response,
    if_match: str | None = Header(default=None, alias="If-Match"),
):
    note = NOTES.get(note_id)
    if note is None:
        raise HTTPException(status_code=404)
    if if_match is None:
        raise HTTPException(status_code=428, detail="If-Match required")
    expected = strong_etag(note["version"])
    if if_match != expected:
        raise HTTPException(status_code=412, detail="stale version")
    note["body"] = payload.body
    note["version"] += 1
    response.headers["ETag"] = strong_etag(note["version"])
    return note
Enter fullscreen mode Exit fullscreen mode

Start the server in one terminal and re-run the suite in another. You want green from the frozen file, not from a rewritten test that lowered 428 to 200.

uvicorn app:app --port 8088 --log-level warning
python -m pytest tests/test_note_if_match.py -q
Enter fullscreen mode Exit fullscreen mode

The client the agent writes later must GET, copy ETag into If-Match, and on 412 re-fetch instead of retrying the same token. Put that in a second test file only after the server suite is green. Do not let the agent invent a retry loop inside the handler; retries belong to the caller that can see a fresh representation.

Where a coding agent belongs in this workflow

The agent is allowed to write app.py and a PATCH client, and it is not allowed to edit tests/test_note_if_match.py. That split is the whole method. If you need a place to run that loop without standing up a local GPU, MonkeyCode’s free model access and free server option can host the agent against the frozen suite. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Keep the product mention in that one role: it is a runner for an already-specified contract, not a source of HTTP semantics. Do not ask the model to choose between 412 and 409, strong versus weak ETags, or whether missing headers should succeed. Those choices are already in the table. If the agent submits a diff that touches the test file, reject the diff even when the new tests are greener, because green tests that encode last-write-wins are how lost updates ship.

Results you should expect from the example

On the seed note, GET returns version: 1 and ETag: "1". A PATCH with If-Match: "1" returns version: 2. A second PATCH that still carries "1" returns 412 and the body stays as writer A left it. A PATCH with no header returns 428. Those four outcomes are the results that matter; they are pass/fail signals, not traffic numbers.

If you deliberately break the handler by commenting out the 428 branch, the require-header test fails first. That is the result you want in review: the contract catches the agent’s most common shortcut before a human has to notice it in staging. If you break only the increment, the bump test fails and the stale-token test may still pass, which tells you the assertions are not duplicates.

Limitations and who should skip this

This pattern does not serialize writes across two resources, so checkout-plus-inventory still needs a transaction or an outbox. Integer versions also do not survive naive client merges; a client that concatenates two bodies after a 412 is doing conflict resolution, which you have not frozen. Weak ETags, CDN filtering of If-Match, and intermediaries that strip unknown headers can still make a correct handler look flaky, so test at the origin, not only through a cache.

Skip this approach when last-write-wins is actually the product, such as analytics counters, presence heartbeats, or true CRDT documents. Skip it when your store cannot atomically compare-and-swap the version column, because a read-modify-write in application memory will lose the race the tests claim to prevent. Skip it when an agent is also allowed to rewrite tests, because then you do not have a freeze, you have a conversation.

Lessons learned

  • Freeze the version window in tests before any PATCH handler exists, or the agent will pick last-write-wins.
  • Prefer 428 on a missing token over a friendly default version; missing headers are how retry scripts bypass your rule.
  • Reject weak ETags and * on this resource so the contract has one comparison, not four.
  • Keep retries in the client after a fresh GET; do not retry the same If-Match inside the server.
  • Treat a test-file edit from the agent as a failed review, even when coverage numbers go up.

You now have a complete small project: a seed note, a red-then-green suite, a handler that only mutates on a matching strong token, and a clear list of domains that should not copy it. Pin the table, run the commands, and only then let an agent type the boilerplate that used to invent lost updates.

Top comments (0)