GET /_user/settings serves the empty document as a real representation: {} with ETag: "0". But check_if_match maps version 0 to "no current representation", so If-Match: "0" — the exact validator the GET handed out — always fails with 412. A client doing read-then-conditional-write (saved-queries.js does exactly this for the roaming recents/saved lists) deadlocks forever on a fresh user: every 412 retry re-reads "0" and fails again.
It went unnoticed because nav.js used to PATCH {nav: …} without If-Match on the first toggle, unconditionally creating the document — after which version ≥ 1 and conditional writes work. #438 removed nav.js, and the e2e suite now fails deterministically on a fresh database (queries.spec.ts › a run is recorded under the Recent disclosure, three PATCH → 412 in the trace of PR #440's CI run).
Fix: in crates/rest/src/handlers/user_settings.rs::check_if_match, evaluate concrete tag lists against version.to_string() including version 0, so the advertised validator round-trips. If-Match: * keeps requiring an actually-stored document. The storage layer is already coherent — all four backends treat Some(0) as "expect absent" and insert.
GET /_user/settingsserves the empty document as a real representation:{}withETag: "0". Butcheck_if_matchmaps version 0 to "no current representation", soIf-Match: "0"— the exact validator the GET handed out — always fails with 412. A client doing read-then-conditional-write (saved-queries.js does exactly this for the roaming recents/saved lists) deadlocks forever on a fresh user: every 412 retry re-reads"0"and fails again.It went unnoticed because nav.js used to PATCH
{nav: …}without If-Match on the first toggle, unconditionally creating the document — after which version ≥ 1 and conditional writes work. #438 removed nav.js, and the e2e suite now fails deterministically on a fresh database (queries.spec.ts › a run is recorded under the Recent disclosure, threePATCH → 412in the trace of PR #440's CI run).Fix: in
crates/rest/src/handlers/user_settings.rs::check_if_match, evaluate concrete tag lists againstversion.to_string()including version 0, so the advertised validator round-trips.If-Match: *keeps requiring an actually-stored document. The storage layer is already coherent — all four backends treatSome(0)as "expect absent" and insert.