Skip to content

fix(app): guard getAuthHeader against missing token (#6142)#6848

Merged
mdmohsin7 merged 1 commit intoBasedHardware:mainfrom
mvanhorn:fix/6142-getauthheader-crash
Apr 20, 2026
Merged

fix(app): guard getAuthHeader against missing token (#6142)#6848
mdmohsin7 merged 1 commit intoBasedHardware:mainfrom
mvanhorn:fix/6142-getauthheader-crash

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

Summary

On v1.0.526, getAuthHeader() in app/lib/backend/http/shared.dart throws a plain Exception('No auth token found') when the user appears signed-in but SharedPreferencesUtil has no cached token. The exception propagates through the async header-builder as an unhandled error and crashes the app. Crashlytics recorded ~3,400 fatals across ~83 users between Mar 22-27, 2026.

This PR converts the throw into a typed AuthTokenUnavailableException and catches it at the sole in-file caller (buildHeaders), logging the condition and letting the request proceed without an Authorization header.

Why this matters

getAuthHeader() sits on every authenticated HTTP path in the app. A raw throw Exception(...) is not something most async callers guard against, so a single user in this bad state generates hundreds of crash reports. The crash count is high (3,400) despite the user count being modest (83), which confirms the exception fires repeatedly as the app retries work.

Changes

Introduce AuthTokenUnavailableException at the top of shared.dart and replace the generic throw with this typed variant. The check logic itself is unchanged: we only throw when the token is empty AND AuthService.isSignedIn() is true, so the semantics that "signed-in-but-no-token is a signal worth surfacing" are preserved - we just stop representing that signal as an unhandled crash.

At the sole in-file caller (buildHeaders around the requireAuthCheck branch), the await getAuthHeader() is wrapped in a try/on AuthTokenUnavailableException that logs via Logger.debug and proceeds without the Authorization header. The downstream HTTP 401 path in makeApiCall already calls AuthService.signOut(), so recovery still happens where it was already wired - we're not adding a new sign-out path, we're just removing the crash that was preventing the 401 path from being reached.

The four other sites that already guard getIdToken() ?? '' (lines 121, 256, 328, 448) are unchanged - they were never at risk because they already handle the null case.

Testing

A small unit test at app/test/unit/backend/http/shared_test.dart covers the typed exception and the caller-guard pattern: exception raises on signed-in-empty-token, caller catches it and omits Authorization, happy path still attaches Bearer <token>. Tests use flutter_test and don't require Firebase/SharedPreferences mocks because they exercise the exception shape directly.

I did not run flutter test locally (flutter SDK not installed in this environment). dart format --line-length 120 clean on both files.

Limitations

  • WebSocket handshake paths (PureSocket.connect, PhoneCallProvider._connectTranscriptionSocket) use buildHeaders(requireAuthCheck: true) but won't see a 401, so a signed-in-no-token user could see socket connections fail silently instead of a crash. Still a net improvement, and the existing HTTP paths will trigger re-auth.
  • No change to AuthService. If the token is persistently missing despite getIdTokenResult(true) returning non-null, that's an underlying auth-state bug worth its own issue.

Fixes #6142

This contribution was developed with AI assistance (Codex).

On v1.0.526, getAuthHeader was throwing a plain Exception('No auth token
found') when the user appeared signed-in but SharedPreferencesUtil had
no cached token. The exception propagated through buildHeaders as an
unhandled async error and crashed the app — ~3,400 Crashlytics fatals
across ~83 users between Mar 22-27.

Wrap the error path in a typed AuthTokenUnavailableException and catch
it at the sole in-file caller (buildHeaders). On catch, log at debug
level and proceed without the Authorization header, letting the normal
backend 401 handling drive re-auth rather than taking down the app.

No changes to AuthService — getIdToken already force-refreshes via
getIdTokenResult(true). No new HTTP wrapping. No other call sites
modified (the four existing `getIdToken() ?? ''` sites already handle
the null case).

Fixes BasedHardware#6142
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 19, 2026

Greptile Summary

This PR converts the bare throw Exception('No auth token found') in getAuthHeader() to a typed AuthTokenUnavailableException and catches it in buildHeaders, letting requests proceed without an Authorization header instead of crashing. The change is minimal, targeted, and correctly preserves the downstream 401 → AuthService.signOut() recovery path in makeApiCall.

Confidence Score: 5/5

Safe to merge — the fix is minimal and correctly eliminates the crash while preserving existing auth-recovery semantics.

All remaining findings are P2: the test file exercises the exception shape via simulation stubs rather than the real implementation, which is a coverage gap but not a correctness defect. The production code change itself is correct and low-risk.

app/test/unit/backend/http/shared_test.dart — tests cover simulation helpers, not the actual buildHeaders catch path.

Important Files Changed

Filename Overview
app/lib/backend/http/shared.dart Introduces AuthTokenUnavailableException and wraps getAuthHeader() call in buildHeaders with a typed catch, replacing the previously unhandled Exception that caused ~3,400 Crashlytics fatals. Logic is unchanged; only the error representation and propagation path differ.
app/test/unit/backend/http/shared_test.dart New test file that verifies the exception shape and catch pattern via simulation stubs, but does not exercise the actual buildHeaders or getAuthHeader implementations, leaving the real catch block untested.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant buildHeaders
    participant getAuthHeader
    participant SharedPrefs
    participant AuthService

    Caller->>buildHeaders: requireAuthCheck: true
    buildHeaders->>getAuthHeader: await
    getAuthHeader->>SharedPrefs: authToken
    SharedPrefs-->>getAuthHeader: ""  (empty)
    getAuthHeader->>AuthService: isSignedIn()
    AuthService-->>getAuthHeader: true

    alt Before this PR
        getAuthHeader-->>buildHeaders: throw Exception("No auth token found")
        buildHeaders-->>Caller: ❌ unhandled crash (Crashlytics fatal)
    else After this PR
        getAuthHeader-->>buildHeaders: throw AuthTokenUnavailableException
        buildHeaders->>buildHeaders: catch → Logger.debug(...)
        buildHeaders-->>Caller: headers (no Authorization key)
        Caller->>Caller: HTTP request → 401
        Caller->>AuthService: signOut() (existing recovery path)
    end
Loading

Reviews (1): Last reviewed commit: "fix(app): guard getAuthHeader against mi..." | Re-trigger Greptile

Comment on lines +5 to +20
Future<String> simulateGetAuthHeader({required bool isSignedIn, required String token}) async {
if (token.isEmpty && isSignedIn) {
throw AuthTokenUnavailableException('No auth token found');
}
return 'Bearer $token';
}

Future<Map<String, String>> simulateBuildHeaders({required Future<String> Function() getAuthHeader}) async {
final headers = <String, String>{};
try {
headers['Authorization'] = await getAuthHeader();
} on AuthTokenUnavailableException {
// Mirrors buildHeaders() behavior in shared.dart: continue without auth header.
}
return headers;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tests simulate rather than exercise real implementation

The tests define local simulateGetAuthHeader and simulateBuildHeaders stubs that mirror the logic in shared.dart but never call the actual buildHeaders() or getAuthHeader(). As a result, the catch block added at line 77 of shared.dart — the core of this fix — has zero test coverage. If a future refactor accidentally removes the on AuthTokenUnavailableException handler, these tests would still pass.

Consider replacing the simulation approach with a test that injects a mock AuthService returning isSignedIn: true and a mock SharedPreferencesUtil returning an empty token, so the real buildHeaders(requireAuthCheck: true) path is exercised end-to-end.

@mdmohsin7 mdmohsin7 merged commit 899a9f6 into BasedHardware:main Apr 20, 2026
1 check passed
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Thanks for the merge, @mdmohsin7. Guarding getAuthHeader against missing tokens is the right call.

Glucksberg pushed a commit to Glucksberg/omi-local that referenced this pull request Apr 28, 2026
…) (BasedHardware#6848)

## Summary

On v1.0.526, `getAuthHeader()` in `app/lib/backend/http/shared.dart`
throws a plain `Exception('No auth token found')` when the user appears
signed-in but `SharedPreferencesUtil` has no cached token. The exception
propagates through the async header-builder as an unhandled error and
crashes the app. Crashlytics recorded ~3,400 fatals across ~83 users
between Mar 22-27, 2026.

This PR converts the throw into a typed `AuthTokenUnavailableException`
and catches it at the sole in-file caller (`buildHeaders`), logging the
condition and letting the request proceed without an Authorization
header.

## Why this matters

`getAuthHeader()` sits on every authenticated HTTP path in the app. A
raw `throw Exception(...)` is not something most async callers guard
against, so a single user in this bad state generates hundreds of crash
reports. The crash count is high (3,400) despite the user count being
modest (83), which confirms the exception fires repeatedly as the app
retries work.

## Changes

Introduce `AuthTokenUnavailableException` at the top of `shared.dart`
and replace the generic throw with this typed variant. The check logic
itself is unchanged: we only throw when the token is empty AND
`AuthService.isSignedIn()` is true, so the semantics that
"signed-in-but-no-token is a signal worth surfacing" are preserved - we
just stop representing that signal as an unhandled crash.

At the sole in-file caller (`buildHeaders` around the `requireAuthCheck`
branch), the `await getAuthHeader()` is wrapped in a `try/on
AuthTokenUnavailableException` that logs via `Logger.debug` and proceeds
without the Authorization header. The downstream HTTP 401 path in
`makeApiCall` already calls `AuthService.signOut()`, so recovery still
happens where it was already wired - we're not adding a new sign-out
path, we're just removing the crash that was preventing the 401 path
from being reached.

The four other sites that already guard `getIdToken() ?? ''` (lines 121,
256, 328, 448) are unchanged - they were never at risk because they
already handle the null case.

## Testing

A small unit test at `app/test/unit/backend/http/shared_test.dart`
covers the typed exception and the caller-guard pattern: exception
raises on signed-in-empty-token, caller catches it and omits
`Authorization`, happy path still attaches `Bearer <token>`. Tests use
`flutter_test` and don't require Firebase/SharedPreferences mocks
because they exercise the exception shape directly.

I did not run `flutter test` locally (flutter SDK not installed in this
environment). `dart format --line-length 120` clean on both files.

## Limitations

- WebSocket handshake paths (`PureSocket.connect`,
`PhoneCallProvider._connectTranscriptionSocket`) use
`buildHeaders(requireAuthCheck: true)` but won't see a 401, so a
signed-in-no-token user could see socket connections fail silently
instead of a crash. Still a net improvement, and the existing HTTP paths
will trigger re-auth.
- No change to `AuthService`. If the token is persistently missing
despite `getIdTokenResult(true)` returning non-null, that's an
underlying auth-state bug worth its own issue.

Fixes BasedHardware#6142

This contribution was developed with AI assistance (Codex).
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.

Android: getAuthHeader crashes with 'No auth token found' (v1.0.526)

2 participants