fix(diagnostics): classify 401/403 as auth-failed so a wrong password stops the retry loop - #170
Merged
Merged
Conversation
… stops the retry loop AGE-107. The 498 `API Error: 401` events from one device were not a client token-refresh loop. Sentry breadcrumbs on the surviving events show a `touch` event immediately before every capture, at irregular human-paced intervals (87s, 199s, 69s, 5s, 61s, 66s) — a person re-tapping Connect, not a backoff timer. The app's automated loops were already correct: events.ts terminates the SSE reconnect loop on ApiAuthError (issue #76). What actually drove it: in v0.4.4 the connection probe counted any HTTP response as a successful health check, so a 401 was classified `ok` and shown to the user as "Health endpoint responded — connection actually works now" while their password was wrong. The user retried for two months. `requireOk` (#114, v0.4.8) stopped the false success, but 401 then fell into the generic `health-failed` bucket — "Likely wrong path, auth, or an old server version" — which still doesn't tell anyone to fix their password. - New `auth-failed` classification: a 401/403 from /global/health means the server is up and reachable and rejected the credentials. Its summary names the status, points at the password and OPENCODE_SERVER_USERNAME, and says the server is fine. It flows straight into the existing failure Alert on both the add and edit connection screens — which is where the password field is, i.e. the re-auth prompt. - It short-circuits before the root/internet probes can downgrade it: a 401 already proves the server answered. - `health-failed` copy no longer blames auth. - `connect auth-failed` joins the noise-gate drop-list. A wrong password is user config, unactionable server-side, already visible in the UI and already trended in PostHog as connection_failed{error_class:"unauthorized"}. `health-failed` and `tls-error` still report. Tests: 6 new (401/403 -> auth-failed, message content, root-unreachable does not override, 404/500/502 stay health-failed, health-failed copy drops "auth", noise gate drops `connect auth-failed` but not a raw `API Error: 401`). 263 pass, tsc --noEmit clean. Co-Authored-By: Paperclip <noreply@paperclip.ing>
omgoshjosh
pushed a commit
to omgoshjosh/opencode-mobile
that referenced
this pull request
Aug 16, 2026
19 commits behind, three versionNames behind. Notable fixes this pulls in that the evaluation build was missing: 401/403 classified as auth-failed so a wrong password stops the retry loop (dzianisv#170), the Sentry noise gate, the waitlist retry queue, and the in-app "a newer version exists" notice for sideloaded installs (dzianisv#179). Three conflicts, all resolved toward keeping both sides: - android/app/build.gradle: took upstream's versionCode 42 / 0.4.15 but kept the BUILD_VERSION_CODE env override. Evaluation builds need a monotonic code from CI so each side-by-side install supersedes the last; upstream release builds leave it unset and use the literal. - app/(tabs)/index.tsx: the group-by picker and UpdateBanner are unrelated additions to the same region. Kept both, banner above the picker so an update notice reads before the list controls. - app/(tabs)/settings.tsx: upstream and this branch independently fixed the same hard-coded "1.0.0". Took upstream's update-aware row but fed it appVersionLabel rather than CURRENT_VERSION -- sideloaded builds share a versionName across many CI builds, so the version alone cannot identify which APK is actually installed, which is the whole reason that row is being read. 382 tests pass, typecheck clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the AGE-107 investigation: why did one device produce 498
API Error: 401events?It was not a token-refresh loop
The client's automated retry paths are correct and were never the cause:
src/stores/events.tsalready terminates the SSE reconnect loop onApiAuthErrorand sets a distinctauthErrorstate (issue bug: recurring 401 API errors on app session (Sentry OPENCODE-MOBILE-1, 65 users) #76 fix).src/stores/sessions.ts/connections.tscatches;sendMessagerethrows into a caughttryinapp/session/[id].tsx.QueryClientin the app is constructed and never used — no polling.Evidence from the live Sentry issue (
7535855407, projectopencode-mobile):All 498 events, spanning 2026-06-08 → 2026-08-04, come from one user on app.version 0.4.4 — a release that is 10 versions stale.
handled: yes,mechanism: generic, tagsconnect.classification/connect.schemepresent → captured bycaptureDiagnostic(), which in v0.4.4 took arawErrorargument and uploaded the rawAPI Error: 401verbatim.The 7 surviving events are spaced 87s, 199s, 69s, 5s, 61s, 66s apart, and each is immediately preceded by
touchbreadcrumbs. That is a person tapping, not a backoff timer.The decisive breadcrumb pair, repeated every time:
The real defect
timedFetchin v0.4.4 returnedok: truefor any HTTP response, so a 401 health check short-circuitedclassify()took. The app told a user with a wrong password that the connection works. They retried for two months.requireOk(#114, shipped v0.4.8) fixed the false success — but 401 then landed in the generichealth-failedbucket, "Likely wrong path, auth, or an old server version", which still doesn't tell anyone to fix their password.This PR
auth-failedclassification. 401/403 from/global/healthproves the server is up, reachable, and rejecting the credentials. The summary names the status, points at the password andOPENCODE_SERVER_USERNAME, and states the server itself is fine. It flows into the existing failureAlertonconnection/addandconnection/[id]— the screens that hold the password field, i.e. the re-auth prompt.server-unreachable.health-failedcopy no longer blames auth.connect auth-failedadded to the noise-gate drop-list. A wrong password is user config: unactionable server-side, already visible in the UI, already trended PII-free in PostHog asconnection_failed{error_class:"unauthorized"}.connect health-failedandconnect tls-errorstill report — an unhealthy box or a broken cert is actionable.Tests
6 new, all pure:
auth-failed, summary mentions the status and credentials and never says "actually works"auth-failedauth-failedsurvives an unreachable root probehealth-failedhealth-failedcopy contains no "auth"connect auth-failed, still reports a rawAPI Error: 401npm test263 pass / 0 fail.tsc --noEmitclean.Note on the quota
This does not change the AGE-105 numbers materially — the noise gate already collapsed this class to ≤1 report per 6h. The value here is that the user now gets told what's wrong, so the loop stops at the source instead of being silently absorbed.