fix(auth): stop infinite SSE retry on 401/403, surface auth failures (#79)
* fix(auth): stop infinite SSE retry on 401/403 and surface auth failures Root cause (Sentry OPENCODE-MOBILE-1, 309 events / 65 users): auth is static HTTP Basic and no code path treated 401 specially. The SSE reconnect loop in events.ts retried on a fixed backoff regardless of cause, so a bad password spammed Sentry and drained battery forever with zero user feedback. Advanced-mode connection save also had no pre-flight check and silently persisted bad credentials as the active connection. - src/lib/api-error.ts: new pure ApiAuthError/isAuthStatus/isAuthError module (node --test covered) so 401/403 are distinguishable from other failures. - src/lib/sdk.ts: request()/events() now throw ApiAuthError for 401/403 instead of a generic Error. - src/stores/events.ts: the SSE loop stops retrying on an auth error and sets a new `authError` flag instead of reconnecting forever; other errors keep the existing backoff. Fires connection_failed (source: sse, error_class: unauthorized) so it's visible in the existing funnel. - app/(tabs)/index.tsx: sessions screen shows an "Authentication Failed" state with a link to the connection edit screen when authError is set. - app/connection/[id].tsx: saving edited credentials for the active connection now reconnects SSE immediately instead of requiring an app restart. - app/connection/add.tsx: Advanced-mode save now runs the same testConnection pre-flight as Quick Connect and shows the same "Connection Failed" alert (with diagnostics/share-report) instead of silently saving bad credentials. Closes #76 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NJKAQ6HAikWGQK7PGZ5Y4E * fix(auth): add Retry button on 401 error state, widen ConnectionTestSource - Authentication Failed screen now offers Retry alongside Check Credentials, calling events store's connect() directly to restart the SSE state machine on transient 401s without leaving the app. - Widen ConnectionTestSource to include 'sse' (events.ts:389's connection_failed track call) and note the activation funnel only filters on source=onboarding. Addresses PR #79 review follow-ups. --------- Co-authored-by: engineer <engineer@gray-knight-m1.local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
38
src/lib/api-error.ts
Normal file
38
src/lib/api-error.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Pure classification of HTTP auth failures, extracted so it's unit-testable
|
||||
// under plain `node --test` without pulling in expo/fetch (sdk.ts is RN-only)
|
||||
// — same pattern as analytics-classify.ts / diagnostics-classify.ts.
|
||||
//
|
||||
// Why this exists: sdk.ts's request()/events() used to throw a generic Error
|
||||
// for every non-2xx response, so call sites (the SSE reconnect loop, screen
|
||||
// error states) had no way to tell "your password is wrong" apart from "the
|
||||
// server is briefly unreachable" — a 401 got treated like any transient
|
||||
// failure and retried forever (see events.ts's reconnect loop / issue #76).
|
||||
|
||||
/** Thrown by sdk.ts's request()/events() when the server responds 401/403,
|
||||
* so call sites can distinguish "bad credentials" from any other failure
|
||||
* (network error, 5xx, timeout) instead of catching a generic Error. */
|
||||
export class ApiAuthError extends Error {
|
||||
readonly status: number
|
||||
constructor(status: number, message: string) {
|
||||
super(message)
|
||||
this.name = "ApiAuthError"
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
/** True for HTTP statuses that mean "your credentials are wrong", as opposed
|
||||
* to transient/network/server failures that should keep retrying. */
|
||||
export function isAuthStatus(status: number): boolean {
|
||||
return status === 401 || status === 403
|
||||
}
|
||||
|
||||
/** Build the right error type for a failed HTTP response. */
|
||||
export function apiErrorFor(status: number, message: string): Error {
|
||||
return isAuthStatus(status) ? new ApiAuthError(status, message) : new Error(message)
|
||||
}
|
||||
|
||||
/** Type guard for call sites (e.g. the SSE reconnect loop) that need to branch
|
||||
* on whether a caught error was an auth failure. */
|
||||
export function isAuthError(error: unknown): error is ApiAuthError {
|
||||
return error instanceof ApiAuthError
|
||||
}
|
||||
Reference in New Issue
Block a user