Skip to content

TT-7583 fix: only warn of a network issue when the upload never completed - #515

Draft
nabalone wants to merge 7 commits into
developfrom
TT-7583_part3_upload-failure-reason
Draft

TT-7583 fix: only warn of a network issue when the upload never completed#515
nabalone wants to merge 7 commits into
developfrom
TT-7583_part3_upload-failure-reason

Conversation

@nabalone

Copy link
Copy Markdown
Collaborator

Follow-up to the TT-7583 auto-save work. The reported bug is that PBT auto-save shows
"Save Failed. Please check your internet connection and try again" when the connection is
fine. This PR fixes the half of that which is about misattributing failures to the network.

The problem

A failed upload item told its callback nothing but success: false. Both itemComplete
handlers (crud/useMediaUpload.ts, components/Uploader.tsx) therefore responded to every
failure by calling setOrbitRetries, which raises the "possible network issue" retry banner
from Sources.tsx — including for a file we rejected locally or a 4xx the server deliberately
returned. The message blamed the user's connection for things that had nothing to do with it.

The change

UploadFailureReason (string enum, store/upload/uploadRetry.ts) names why an item failed:

reason when connection problem?
UnsupportedType / TooBig / LocalWriteFailed never left the client no
Rejected server answered 4xx (incl. 429) no
ServerError server answered 5xx no
Timeout request sent, timed out yes
NoResponse never reached the server yes

suggestsConnectionProblem() is the single place that decides, and both itemCompletes now
call it instead of duplicating the judgment.

Behavior narrows: 5xx and 429 no longer raise the connection warning — both mean we reached
the server. Retry policy is untouched: isRetryableUploadStatus still governs the retry loop
and is now a separate question from "is the user's connection bad".

Why a reason code rather than a bool or a status number

Two alternatives were considered and rejected:

  • Tri-state success (ok | retryable | terminal). Real appeal — it removes the redundant
    success: false + failure-object encoding. Rejected because a string union is truthy, so any
    missed if (success) (there is one at useMediaUpload.ts) would silently start reporting
    success; because "terminal failure" already means something different and specific in this file
    (onTerminalFailure = PUT exhausted, row deleted, file queued for later retry); and because it
    can't carry statusNum.
  • Pass the HTTP status, derive retryability at each call site. Right instinct, fatal detail:
    isRetryableUploadStatus(undefined) returns true (at the retry layer "no status" means "no
    response"), and local rejections have no status at all — so an unsupported file type would
    compute as a network problem, which is the exact bug being fixed.

The second commit is the interesting one

uploadFile's onerror coerced a dropped connection into a fake 500 (httpStatus || 500;
httpStatus is 0 when the browser never got a response). So a genuine connection drop on the
S3 PUT
classified as ServerError and stayed silent — the one case the warning exists for.
The signal was being destroyed a layer below where the classifier read it, so the unit tests on
the classifier could not have caught it. Found by building the fake-error harness, not by review.

actions.failureReason.test.ts now drives real XMLHttpRequest/Axios stubs end-to-end and
asserts the reason that actually reaches cb for all seven paths.

⚠️ Before marking ready

  • Revert 6ecd5a1c (TT-7583 TEMPORARY: localStorage-driven fake upload failures). It is
    isolated in one commit precisely so this is a single operation. It adds
    store/upload/fakeUploadFailure.ts plus three blocks in actions.tsx marked
    TEMPORARY (TT-7583 manual test).
  • This branch does not rebase cleanly onto develop. git rebase origin/develop conflicts in
    crud/useWavesurferRegions.tsx, between a57c1342 (auto-segment guard, inherited from the
    part2 branch — not from this PR's work) and a5ca8ff5 (TT-7377, "don't reload blob when
    editing"). Left unresolved deliberately: it's a judgment call about the auto-segment feature.
  • This branch carries unmerged part2 commits (bc7d3b8e + its revert ba9802da,
    0b107780, a57c1342). If part2 lands separately, rebase to drop them.

Manual test plan

Not yet run against the app — verified by unit/integration tests only. With the harness commit
still present, from the browser console (no reload needed):

localStorage.apmFakeUpload = 'put:0'     // PUT never reaches S3   -> NoResponse   banner
localStorage.apmFakeUpload = 'put:408'   // PUT times out          -> Timeout      banner
localStorage.apmFakeUpload = 'put:403'   // S3 refuses             -> Rejected     silent
localStorage.apmFakeUpload = 'put:500'   // S3 fails               -> ServerError  silent
localStorage.apmFakeUpload = 'post:0'    // POST gets no response  -> NoResponse   banner
localStorage.apmFakeUpload = 'post:403'  // API refuses            -> Rejected     silent
localStorage.apmFakeUpload = 'post:500'  // API fails              -> ServerError  silent
delete localStorage.apmFakeUpload

Record and save a clause on Careful Speech or PBT for each. Two things to watch:

  1. The banner appears for exactly the three banner rows and nothing else.
  2. Everything else is identical across all seven — save-failed snackbar, file landing in the
    pending-uploads queue, retry loop stopping. If anything beyond the banner changes with the
    reason, that's a bug in this change.

The highest-value single check is put:0 vs put:500 back to back — that pair is exactly the
distinction the fix turns on, and put:0 is the case commit 2 repairs.

Two cases need no harness and exercise the sendError path instead of the network path: upload
a file with an extension outside .wav/.mp3/.m4a/.ogg/.webm/.pdf/.png/.jpg (UnsupportedType),
and one over the size limit (TooBig). Neither should raise the banner.

Noticed, not addressed

The PUT retry loop (actions.tsx) runs all 5 attempts even for a 403, unlike the POST loop which
bails on non-retryable statuses. Visible as a ~6s delay on put:403. Pre-existing; out of scope.

Verification

npm run typecheck:web, ESLint on changed files, and Prettier are clean; the 6 affected Jest
suites (58 tests) passed at the time the work was done. Per request, no CI/Devin checks were run
or waited on for this PR.

🤖 Generated with Claude Code

nabalone and others added 7 commits August 18, 2026 09:27
…eted

A failed upload item told its callback nothing but success: false, so both
itemComplete handlers responded to every failure by calling setOrbitRetries --
raising a "possible network issue" retry banner for a file we rejected locally
or a 4xx the server deliberately returned.

Give the callback a reason instead. UploadFailureReason names why the item
failed (unsupported type, too big, local write, 4xx, 5xx, timeout, no
response); suggestsConnectionProblem is the single place that decides which of
those is evidence about the user's connection, and only a request that never
completed qualifies. A status number alone could not carry this: local
rejections never reach the network and have no status, and undefined already
means "no response" to isRetryableUploadStatus.

This narrows the warning: 5xx and 429 no longer raise it, since both mean we
reached the server. Retry policy is unchanged -- isRetryableUploadStatus still
governs the retry loop, and is now separate from the connectivity question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ttings

PhraseBackTranslateStepSettings seeded its artifact type from getTypeId, which
returns the local Orbit record id. That value was emitted into the step settings
JSON and persisted verbatim, but every reader decodes settings.artifactTypeId
with remoteIdGuid (and SelectArtifactType hands the other step-settings dialogs
remote ids). The `?? id` fallback in those readers hid it locally; once the step
synced, peers could not resolve the GUID.

Translate the local id to its remote id when seeding, keeping the local id when
there is no mapping (offline-only artifact types have no remote id).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cases

- useWavesurferRegions: a too-short first gap indexed result[-1] and threw;
  skip it so the gap joins the next region instead.
- useWavesurferRegions: don't drop the only region when the whole clip is
  shorter than the minimum length.
- useGuidedPhraseSegments: note the remaining case where auto-segment
  legitimately yields nothing and the bootstrap poll keeps spinning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eted

A failed upload item told its callback nothing but success: false, so both
itemComplete handlers responded to every failure by calling setOrbitRetries --
raising a "possible network issue" retry banner for a file we rejected locally
or a 4xx the server deliberately returned.

Give the callback a reason instead. UploadFailureReason names why the item
failed (unsupported type, too big, local write, 4xx, 5xx, timeout, no
response); suggestsConnectionProblem is the single place that decides which of
those is evidence about the user's connection, and only a request that never
completed qualifies. A status number alone could not carry this: local
rejections never reach the network and have no status, and undefined already
means "no response" to isRetryableUploadStatus.

This narrows the warning: 5xx and 429 no longer raise it, since both mean we
reached the server. Retry policy is unchanged -- isRetryableUploadStatus still
governs the retry loop, and is now separate from the connectivity question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
uploadFile's onerror coerced a dropped connection into a fake 500:
httpStatus is 0 when the browser never got a response, and `httpStatus || 500`
turned that into a status that claims we reached the server. The PUT loop then
passed 500 down, so the preceding commit's classifier read it as ServerError
and stayed silent -- for the single case the warning exists to report.

Reject with statusNum 0 instead and leave httpStatus undefined, so a genuine
connection drop classifies as NoResponse. Retry behavior is unchanged:
uploadErrorHttpStatus ignores an out-of-range statusNum and falls through to
isRetryableUploadStatus(undefined), which is still retryable. The user-facing
message now renders "(no response)" for 0 as well as undefined.

Add actions.failureReason.test.ts, which drives real XMLHttpRequest and Axios
stubs end to end and asserts the reason that actually reaches cb for all seven
paths. The unit tests on the classifier could not have caught this -- the
signal was being destroyed a layer below where they read it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
REVERT THIS COMMIT BEFORE MARKING THE PR READY. It exists only so the
connection-problem warning can be exercised by hand without arranging a real
network fault, and it is deliberately isolated in one commit so dropping it is
a single operation.

From the browser console, no reload needed:

  localStorage.apmFakeUpload = 'put:0'     PUT never reaches S3   -> NoResponse  (warns)
  localStorage.apmFakeUpload = 'put:408'   PUT times out          -> Timeout     (warns)
  localStorage.apmFakeUpload = 'put:403'   S3 refuses             -> Rejected    (silent)
  localStorage.apmFakeUpload = 'put:500'   S3 fails               -> ServerError (silent)
  localStorage.apmFakeUpload = 'post:0'    POST gets no response  -> NoResponse  (warns)
  localStorage.apmFakeUpload = 'post:403'  API refuses            -> Rejected    (silent)
  localStorage.apmFakeUpload = 'post:500'  API fails              -> ServerError (silent)
  delete localStorage.apmFakeUpload        back to normal

Adds fakeUploadFailure.ts plus three blocks in actions.tsx, each marked
TEMPORARY (TT-7583 manual test).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant