Skip to content

Fix stale SHA conflict handling in updateContent and saveFile#162

Open
deferredreward wants to merge 1 commit intounfoldingWord:masterfrom
deferredreward:bugfix/stale-sha-conflict-handling
Open

Fix stale SHA conflict handling in updateContent and saveFile#162
deferredreward wants to merge 1 commit intounfoldingWord:masterfrom
deferredreward:bugfix/stale-sha-conflict-handling

Conversation

@deferredreward
Copy link
Copy Markdown

Summary

Fixes multi-tab save failures caused by stale SHA conflicts being mishandled as missing branches.

Before: Any PUT error in updateContent() was treated as "branch doesn't exist" → new_branch creation attempt → cascade failure → generic "Error creating file" shown to user.

After: updateContent() checks the HTTP status:

  • 409/422 (stale SHA): Re-fetches the file to get the current SHA, retries PUT with fresh SHA
  • 404 (branch missing): Creates new branch (existing behavior preserved)
  • Other errors: Propagated as-is

saveFile() now only falls through to createContent() on 404 (file genuinely missing), since updateContent handles stale SHA retries internally.

Root Cause

When two tabs edit the same file, Tab B's save changes the file's SHA on the server. Tab A still holds the old SHA. When Tab A tries to save:

  1. PUT with stale SHA → 409 Conflict
  2. updateContent catch: assumes branch missing → retries PUT with new_branch → fails
  3. saveFile catch: falls through to createContent() → file already exists → "Error creating file."
  4. User sees: "Error saving file! File could not be saved."

Files Changed

  • src/core/gitea-api/repos/contents/contents.tsupdateContent(): status-aware error handling with SHA re-fetch on 409/422
  • src/components/file/helpers.jssaveFile(): only fall through to createContent on 404

Verified

Tested against tc-create-app with exact multi-tab reproduction:

  1. Tab A edits and saves ✅
  2. Tab B edits and saves ✅ (SHA changes on server)
  3. Tab A edits and saves ✅ — 409 detected, fresh SHA fetched, PUT retried, save succeeds

Console evidence:

updateContent: PUT failed { status: 409, filepath: "tn_GEN.tsv", sha: "abc123..." }
updateContent: stale SHA detected, re-fetching latest file for current SHA...
updateContent: retrying PUT with fresh SHA { oldSha: "abc123...", newSha: "def456..." }

No error popup. No "Error creating file." No auth/login prompt.

Closes #161

🤖 Generated with Claude Code

updateContent() previously caught ALL PUT errors and assumed the branch
didn't exist, falling through to a new_branch creation attempt. When the
actual error was a 409 Conflict (stale SHA from another tab saving the
same file), this created a cascade failure ending in a generic "Error
creating file" message.

Fix: updateContent now checks the HTTP status code before deciding what
to do:
- 409/422 (stale SHA): re-fetch the file to get the current SHA from
  the server, then retry the PUT once with the fresh SHA
- 404 (branch missing): create new branch (existing behavior)
- Other errors: propagate as-is

saveFile() had the same over-broad catch pattern, falling through to
createContent() on any updateContent error. Now it only tries
createContent for 404 (file genuinely missing), since updateContent
handles stale SHA retries internally.

Verified with multi-tab reproduction:
  Tab A saves → Tab B saves (SHA changes) → Tab A saves again
  Before: "Error saving file! File could not be saved."
  After: 409 detected → fresh SHA fetched → PUT retried → save succeeds

Closes unfoldingWord#161

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@deferredreward
Copy link
Copy Markdown
Author

Test Results

Multi-tab stale SHA save — VERIFIED ✅

Repro (tc-create-app):

  1. Tab A: open a TSV file, edit cell, save → succeeds
  2. Tab B: open same file in new tab, edit different cell, save → succeeds (SHA changes on server)
  3. Tab A: edit again, save
  4. Before fix: PUT fails with 409 → inner catch assumes "branch doesn't exist" → retries with new_branch → fails → saveFile falls through to createContent → "Error creating file." → user sees "Error saving file!"
  5. After fix: PUT fails with 409 → readContent fetches latest SHA → retry PUT with fresh SHA → save succeeds transparently

Console evidence from verified test run:

updateContent: PUT failed { status: 409, filepath: "tn_GEN.tsv", sha: "abc..." }
updateContent: stale SHA detected, re-fetching latest file for current SHA...
updateContent: retrying PUT with fresh SHA { oldSha: "abc...", newSha: "def..." }

No error popup. No "Error creating file." No auth/login prompt.

Not Yet Tested

  • updateContent with 404 (branch genuinely missing) — should create new branch
  • updateContent with 500 server error — should propagate error, not retry
  • saveFile with 404 from updateContent — should fall through to createContent
  • saveFile with 409 from updateContent after retry also fails — should propagate, not fall through to createContent
  • Triple-tab scenario: Tab A saves, Tab B saves, Tab C saves, Tab A saves (double stale SHA)

@deferredreward
Copy link
Copy Markdown
Author

Remaining Test Cases

These scenarios test edge cases in the stale SHA handling and error routing.

1. updateContent with real 404 — branch genuinely missing

Setup: Use an app that calls updateContent (e.g., tc-create-app or gateway-edit)
Steps:

  1. Delete the user's working branch on DCS (e.g., deferredreward-tc-create-1)
  2. Make an edit and save
    Expected: PUT returns 404 → updateContent falls through to new_branch creation path → branch is created → save succeeds
    Validates: The 404 → new_branch path still works after the refactor

2. updateContent with 500 server error

Setup: Mock or intercept the PUT endpoint to return 500
Steps:

  1. Make an edit and save
    Expected: Error propagates up to the caller (saveFileuseRetrySave). No retry, no readContent fetch, no createContent fallback. User sees appropriate error.
    Validates: Non-conflict errors are not incorrectly retried

3. saveFile with 404 from updateContent — file doesn't exist on branch

Setup: A scenario where the file exists on master but not on the user branch (e.g., first edit of a new file)
Steps:

  1. Select a file that hasn't been edited before on this branch
  2. Make an edit and save
    Expected: updateContent returns 404 → saveFile catches it → falls through to createContent → file created on branch → save succeeds
    Validates: The createContent fallback still works for genuine "file not found" cases

4. updateContent retry fails — stale SHA even after re-fetch

Setup: This is a theoretical edge case where the file changes again between the readContent (SHA re-fetch) and the retry PUT
Steps: Would require three near-simultaneous saves from three tabs

  1. Tab A saves (changes SHA to X)
  2. Tab B saves (changes SHA to Y)
  3. Tab C tries to save with original SHA → 409 → re-fetches → gets SHA Y → retries PUT
  4. Simultaneously, Tab A saves again (changes SHA to Z)
  5. Tab C's retry PUT now has SHA Y but server has SHA Z → 409 again
    Expected: Second 409 propagates as an error (no infinite retry loop). User sees save error and can retry manually.
    Validates: Single retry prevents infinite loops

5. Concurrent rapid saves from same tab

Setup: Open tc-create-app, edit a TSV file
Steps:

  1. Edit cell A, save immediately
  2. Before save completes, edit cell B, save immediately
    Expected: Both saves complete (second may hit 409 and retry with fresh SHA). No data loss, no error popup.
    Validates: Rapid sequential saves don't create a deadlock or race condition

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

updateContent treats stale SHA conflicts (409) as missing branch, causing save failures in multi-tab usage

1 participant