Repository navigation
Conditional writes (If-Match/If-None-Match) are silently ignored → lost-update hazard #1395
Description
Summary
Harper emits real, meaningful ETags on reads, but honors none of the HTTP write-side preconditions (If-Match, If-None-Match) on PUT/PATCH/DELETE. Every conditional write succeeds and mutates state regardless of the precondition, so a client using If-Match for optimistic concurrency is silently unprotected — a lost-update hazard.
This is the write-side of the long-standing "no safe compare-and-set / If-Match ignored" gap.
Severity
Medium — no single-write corruption, but two concurrent writers using If-Match for optimistic concurrency both succeed and one update is silently lost. The danger is that it's a silent no-op on a standard mechanism: clients believe they're protected and aren't.
Repro (self-contained)
On any @export table Doc with a record r1:
# 1. read r1, capture its ETag
curl -i http://localhost:9926/Doc/r1 # -> 200, ETag: "<E>"
# 2. conditional update with a WRONG etag -> should be 412 Precondition Failed
curl -i -X PUT -H 'Content-Type: application/json' \
-H 'If-Match: "deadbeef"' \
-d '{"id":"r1","v":2}' http://localhost:9926/Doc/r1
# ACTUAL: 204, record updated to v:2 (EXPECTED: 412, unchanged)
# 3. lost-update proof: two clients hold the same etag <E>, both PUT If-Match: "<E>"
# -> both return 204; the second silently overwrites the first. EXPECTED: 412 on the 2nd.
Full matrix observed (method × existence × precondition): honored = 0, ignored-hazard = 5, wrong-status = 7.
If-Match: <wrong|*|live-etag>on PUT/PATCH/DELETE → 204/200 + state changed (should be 412 on mismatch).If-Match: *on PUT-to-absent → 204 create (should be 412).PATCH-to-missing → 204 implicit create (should be 404);DELETE-of-missing → 200 (should be 404).If-None-Match: *on collection POST → 409 (intrinsic PK conflict; the header itself is ignored). Note: collection POST to a duplicate id does correctly 409 — that's an intrinsic check, not precondition handling.
Root cause
server/REST.ts reads only if-none-match, and only on the read path. There is no if-match / if-unmodified-since / 412 handling anywhere on the write path.
Recommendation
Either (a) implement If-Match / If-None-Match → 412 on writes (standard HTTP optimistic concurrency, pairs with the ETag already emitted), fixing the wrong-status PATCH/DELETE-to-missing edges alongside; or (b) document loudly that conditional writes are unsupported and silently ignored. Option (a) closes a real lost-update vector.
Surfaced by the QA-explorer exploratory campaign against Harper 7aaa5a152. The cited file (server/REST.ts) is byte-identical to main @6797f091d, so this reproduces on main. A ready-to-promote regression test (full precondition matrix) exists. Filed by Claude (Opus 4.8) for @kris.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity