Software reliability · An executed HTTP experiment

Inspect what survives the second save

A successful response is useful only when the intended words remain in the stored document.

  1. Two saves can lose one changeA stale whole-document request can restore a field its author did not mean to edit.
  2. A 412 rejection is a pauseReplacing only the version token and retrying the old body can still erase the earlier edit.
  3. Compare before trying againPreserve the draft, inspect the latest version and apply the intended change against it.

Two people edit different fields in the same document. Both see that their saves succeeded. The last saved document nevertheless lacks one person’s change. The second person did not deliberately undo it: their request carried a stale copy of the entire document, including a field they had not touched.

We reproduced this with a small local HTTP server and two invented clients. A more revealing result came after adding a version check. The server correctly rejected the stale save. Then a seemingly helpful retry—fetching the latest version token and sending the same old document again—erased the earlier edit anyway. This is a controlled example, not a defect found in a named editor.

The unchanged field travels with the save

The document had just two fields. Both clients first read the same starting state and received the strong tag ETag: "v1":

{"title":"Draft","note":"Check figures"}

Client A changed only the title to Ready. Client B, still looking at the original copy, changed only the note to Figures checked. Their whole-document replacements were:

A: {"title":"Ready","note":"Check figures"}
B: {"title":"Draft","note":"Figures checked"}

With no version condition, A’s PUT returned 204, followed by B’s PUT returning 204. The final stored title was Draft. The server had accepted exactly what B sent. The old title was part of that request, even though B had only intended to edit the note.

On a disposable staging record, the acceptance test must make both clients read before either writes. Save A first, then submit B’s original replacement and fetch the stored document. A sequence that reloads B’s client immediately before editing skips the stale-copy condition.

The first rejection worked

RFC 9110 defines If-Match for conditional requests and requires strong comparison. We repeated the sequence with both clients sending If-Match: "v1". A’s write advanced the server to "v2". B’s stale condition failed with 412 Precondition Failed. A’s title remained Ready. B’s note had not been saved; the interface therefore needs to preserve B’s unsaved draft.

The stored result after B’s attempt

Recovery pathB’s resultStored titleStored note
No version condition204DraftFigures checked
Original tag, stale body412ReadyCheck figures
New tag, same stale body204DraftFigures checked
Review and merge the note204ReadyFigures checked
Executed synthetic HTTP cases. The last two rows are alternative continuations after A’s save.

These are outcomes of our synthetic loopback fixture. The last two rows are alternative continuations after A’s save, not successive edits to a real product.

Our earlier ETag experiment asked whether a returning client receives corrected HTML. Here the version condition controls whether a client may replace the current document. The two problems share a header name but require different tests.

A fresh token can authorize an old mistake

After the rejection, our unsafe client fetched the latest document and its "v2" tag. It kept B’s original body and changed only the request header to If-Match: "v2". That request returned 204. The title reverted to Draft. The server’s comparison was accurate; the client had attached a current token to content it had never reconciled with the current version.

After a 412 rejection, a blind retry with a new token loses the earlier title; reviewing and merging the independent note edit retains both changes.
The two paths continue the same rejected save in our synthetic two-field test. The green result assumes independent edits and a reviewed merge.

In the successful recovery, we retained the newly fetched title, applied B’s intended note change, and submitted {"title":"Ready","note":"Figures checked"} with the tag of the version we had reviewed. Both edits survived. MDN’s conditional-request guide describes comparing versions or restarting from the newest one after a conflict. For this example, the useful comparison has three inputs: the document B originally read, B’s proposed document, and the latest server document. The old and latest documents alone do not show which field B deliberately changed.

This merge worked because the intended edits affected independent fields. If both editors change the title, the software cannot infer which new title should win. Even different fields can be linked by a business rule. If another writer saves while B reviews the conflict, the new conditional attempt must be allowed to fail again.

The shortcuts that leave the door open

We sent If-Match: * with B’s stale body. The resource existed, so the server accepted it and erased A’s title. The wildcard checks existence; it does not identify the version B read. We also sent W/"v1" against a strong "v1" tag and received 412, consistent with the standard’s strong-comparison rule.

We then made version conditions optional. A saved conditionally, but B omitted the header and its obsolete replacement succeeded. A strict variant rejected the missing condition with 428 Precondition Required, the status defined for this purpose in RFC 6585. That status is optional in HTTP; test the actual endpoint’s policy rather than assuming every server enforces it.

The check must hold when the write happens

We deliberately split the version check from the write with an asynchronous barrier. Two requests checked "v1" before either committed. Both then returned 204; their intended changes did not both survive. The working in-memory variant compared and replaced without yielding between those operations. That protects only this one process, not several application servers sharing storage.

A deployed implementation needs the equivalent of “update this record only while its stored version still equals the version I read” at the storage boundary, followed by a check that the update actually occurred. PostgreSQL documents that a Read Committed UPDATE rechecks its WHERE condition after a competing update. We did not run a database experiment; that documentation explains why an earlier application-side check is a different guarantee.

Test recovery by inspecting the surviving words

For an autosave acceptance test, retain the two original bodies, both received tags, the order of writes, each response and the final stored fields. Exercise what the interface does after 412. A test that stopped at the rejection would have passed our unsafe retry. Preserve the unsaved draft and let the editor compare it with the latest document before authorizing another write. If the product offers an intentional overwrite, show what will be lost.

Test the deployed route as well. A redirect can change a submission’s method and body, and a value can be corrupted before it is sent. Neither is repaired by a version condition. The decisive assertion names the words that survive, not merely the number of successful requests.

Standards, documentation and test boundary

Checked 22 September 2026. Eighteen assertions ran in Node.js v22.14.0 against one loopback HTTP server. Both documents and writers were invented. The fixture covers the stated single-tag and wildcard cases, not the complete HTTP grammar. No commercial editor, browser autosave interface, database, proxy, authentication system or network failure was tested.