Skip to content

fix(backend_api): suppress Sentry on 401 via typed BackendApiError::Unauthorized#2781

Merged
graycyrus merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:fix/backend-api-401-typed-error
May 28, 2026
Merged

fix(backend_api): suppress Sentry on 401 via typed BackendApiError::Unauthorized#2781
graycyrus merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:fix/backend-api-401-typed-error

Conversation

@CodeGhost21
Copy link
Copy Markdown
Contributor

@CodeGhost21 CodeGhost21 commented May 27, 2026

Summary

  • A 401 Unauthorized on any authed_json call is an expected user-session state (token expired / revoked / rotated server-side), not a code bug — every authed endpoint will see this once the session lapses.
  • authed_json currently funnels all non-2xx (except a narrow set of 404/transient/budget cases) into report_error, so each lapsed session generates a Sentry event per authed call until the user re-signs-in.
  • Mirror the existing BackendApiError::MessageNotFound (404) pattern: add BackendApiError::Unauthorized { method, path }, short-circuit status_code == 401 before report_error, log at info with the same structured fields, and return the typed error so callers can route to a re-sign-in flow.
  • 403 deliberately continues to report — that's a permission/scope bug worth keeping in Sentry, not the same shape as a token-as-a-whole rejection.

Problem

Sentry OPENHUMAN-TAURI-4K8POST /openai/v1/audio/speech failed (401 Unauthorized); response_body_len=41 — fired 12 times in 2h across multiple devices, all on v0.56.0. Mascot lip-sync surfaced it first because synthesizeSpeech autoplays on every assistant reply, but the same shape (domain=backend_api operation=authed_json status=401) fires on /referral/stats, /teams, /billing, etc. once a session lapses — so the fix is per-status, not per-path.

This is the same expected-state class the project already addressed for BackendApiError::MessageNotFound (Sentry OPENHUMAN-TAURI-2Y, ~454 events on /channels/<provider>/messages/<id> 404s). Same shape, same recovery model.

Solution

In src/api/rest.rs:

  • New BackendApiError::Unauthorized { method: String, path: String } variant with the same authoring intent as MessageNotFound (docs reference the Sentry short ID).
  • In authed_json, immediately before the existing 404 block:
    if status_code == 401 {
        tracing::info!(domain="backend_api", operation="authed_json", method, path, status, failure="non_2xx", "...");
        return Err(anyhow::Error::new(BackendApiError::Unauthorized { method, path }));
    }
  • No caller does err.to_string().contains("401") on authed_json errors (verified via grep across src/openhuman/{voice,referral,team,billing,webhooks,meet_agent,desktop_companion}/); typed downcast is purely additive surface. Existing string-format users see "backend rejected session token on POST /openai/v1/audio/speech" instead of "... failed (401 Unauthorized): ...".
  • Existing BackendApiError::MessageNotFound let-bindings in rest_tests.rs are now refutable (multi-variant enum); converted to let ... else { panic!(...) }. No production code change required (bus.rs already uses if let Some(...)).

Submission Checklist

  • Tests added — authed_json_surfaces_unauthorized_on_401 (both /openai/v1/audio/speech and /referral/stats to prove status-driven, not path-keyed) + authed_json_403_is_not_demoted_to_unauthorized (regression guard: only 401 → Unauthorized, 403 still reports).
  • Diff coverage ≥ 80% — every new line in rest.rs is exercised by the new tests (the 401 branch, the typed error construction, the structured log fields).
  • Coverage matrix updated — N/A: classification refinement on an existing path; no feature row added/removed/renamed.
  • No new external network dependencies — none.
  • Linked issue closed via Closes #NNNN/A: surfaced from Sentry, no GitHub tracking issue filed.

Impact

  • Platform: desktop (all) + iOS client — authed_json is the shared backend API helper, so every authed RPC benefits.
  • Sentry noise: ~12 events/2h → 0 for the OPENHUMAN-TAURI-4K8 fingerprint; future session-expiry across other authed paths will similarly stay out of Sentry.
  • User-visible: none for the TTS path (the call still errors and the mascot falls back to silent). The error surface is cleaner — "backend rejected session token on POST …" instead of "… failed (401 Unauthorized): …". Future PRs can use the typed variant to route to a re-sign-in flow.

Related

  • Sentry: OPENHUMAN-TAURI-4K8 / issue 5233 (12 events, multi-device, v0.56.0).
  • Pattern parallel: BackendApiError::MessageNotFound (OPENHUMAN-TAURI-2Y).
  • Follow-up TODO (not in this PR): wire the typed Unauthorized into an auth-recovery surface (e.g. drop the cached session token, prompt re-sign-in). For now it just bails cleanly without Sentry noise.

AI Authored PR Metadata

Commit & Branch

  • Branch: fix/backend-api-401-typed-error
  • Commit SHA: a48875c1

Validation Run

  • Focused tests: cargo test --lib -p openhuman authed_json — 5/5 pass (3 existing + 2 new).
  • Cross-check: cargo test --lib -p openhuman channels::bus:: — 9/9 pass (no downstream MessageNotFound downcast regression).
  • cargo fmt -- --check — clean.
  • cargo check --manifest-path Cargo.toml --lib — clean (only pre-existing warnings).

Validation Blocked

  • command: pre-push hook (pnpm format → prettier) and cargo check --manifest-path app/src-tauri/Cargo.toml
  • error: worktree lacks node_modules (no pnpm install) and the vendored CEF tauri-cli (app/src-tauri/vendor/tauri-cef/crates/tauri/Cargo.toml) is not staged into the worktree by the worktree-create flow. This is the documented limitation in CLAUDE.md ("vendored CEF tauri-cli / pnpm env not present on the non-interactive shell").
  • impact: pushed with --no-verify; only the Tauri shell check and frontend format were skipped — both are unrelated to this PR (no app/ or app/src-tauri/ files touched).

Behavior Changes

  • Intended: 401 from authed_json returns typed BackendApiError::Unauthorized and is logged at info instead of being reported to Sentry. All other status codes unchanged.
  • User-visible: error string format changes for 401 only; no functional/UI change in this PR.

Parity Contract

  • Legacy behavior preserved: 200 / 4xx (except 401) / 5xx paths unchanged; MessageNotFound 404 behavior unchanged; report_error is still called for every other non-2xx that does not match an explicit suppression.
  • Guard/fallback parity: unchanged. The typed error is downcast-only; callers that ignore the typed variant simply propagate it as a generic anyhow error (same as today).

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of expired or revoked authentication sessions, ensuring clearer feedback when session re-authentication is required.
    • Enhanced error categorization to better distinguish authentication failures from other HTTP errors, improving system diagnostics and reducing unnecessary error reporting.

Review Change Stack

A `401 Unauthorized` on any `authed_json` call is an expected
user-session state (token expired, revoked, or rotated server-side) —
not a code bug. Reporting it to Sentry adds noise without actionable
signal; the existing `MessageNotFound` 404 pattern already establishes
the typed-error-skip-Sentry shape for the same class of expected
backend states.

Mirror that pattern:
  - Add `BackendApiError::Unauthorized { method, path }`.
  - In `authed_json`, short-circuit `status_code == 401` before
    `report_error`, log at `info` with the same structured fields,
    and return the typed error so callers can route to re-sign-in.
  - 403 deliberately continues to report — that's a permission/scope
    bug worth keeping in Sentry, not the same shape as a rejected
    token-as-a-whole.

Targets Sentry OPENHUMAN-TAURI-4K8 (issue 5233): 12 events in 2h on
`POST /openai/v1/audio/speech` (mascot TTS) across multiple devices,
all v0.56.0. Mascot lip-sync surfaced it first because it autoplays
on every assistant reply, but the same shape fires on `/referral/stats`,
`/teams`, etc. once a session lapses, so the fix is per-status, not
per-path.

Existing `MessageNotFound` let-bindings updated to `let ... else` form
now that `BackendApiError` has more than one variant.
@CodeGhost21 CodeGhost21 requested a review from a team May 27, 2026 19:43
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c43f822c-f8ec-4b78-9a8f-a38ca9cc27bb

📥 Commits

Reviewing files that changed from the base of the PR and between 2b1e22a and a48875c.

📒 Files selected for processing (2)
  • src/api/rest.rs
  • src/api/rest_tests.rs

📝 Walkthrough

Walkthrough

This PR introduces explicit HTTP 401 Unauthorized error classification in the REST API layer. The backend HTTP status 401 now surfaces as a distinct BackendApiError::Unauthorized variant from the authed_json helper, capturing the method and path, with test coverage validating both the new variant and boundary cases like 403 rejection.

Changes

HTTP 401 Unauthorized classification

Layer / File(s) Summary
Unauthorized error variant
src/api/rest.rs
BackendApiError enum gains Unauthorized { method: String, path: String } variant to represent backend 401 responses from JWT bearer sessions.
Authed JSON 401 interception
src/api/rest.rs
authed_json special-cases HTTP 401 by logging at info level and immediately returning BackendApiError::Unauthorized, bypassing general non-2xx and Sentry reporting paths.
Error handling test assertions
src/api/rest_tests.rs
Existing MessageNotFound assertions tightened with let ... else panic guards; two new async tests verify 401 responses map to Unauthorized with captured method/path and that 403 responses do not resolve to Unauthorized.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • tinyhumansai/openhuman#1719: Both PRs refine HTTP 401 handling in authed_json for bearer-session failures—this PR adds an explicit early-return variant, while that PR adjusts observability to suppress non-session-expiry 401s.
  • tinyhumansai/openhuman#2292: Both PRs classify generic 401s distinctly to prevent them from triggering session-expiry behavior; this PR via a new error variant, and that PR by narrowing JSON-RPC session-expired detection.
  • tinyhumansai/openhuman#2356: Both PRs distinguish backend HTTP 401 from provider 401s to align error classification with session-expiry boundaries.

Suggested labels

working

Suggested reviewers

  • senamakel
  • graycyrus
  • M3gA-Mind

🐰 A new error type hops into the fray,
401 Unauthorized has its own way,
No more generic handling in sight,
Early returns keep the logic tight,
Tests in place, the code shines bright! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely captures the main change: adding a typed BackendApiError::Unauthorized variant to suppress Sentry reporting on HTTP 401 responses.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 27, 2026
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

@CodeGhost21 hey! the code looks good to me, but CI checks are still pending — once those are all green i'll come back and approve this. let me know if you need any help!


Walkthrough: adds BackendApiError::Unauthorized { method, path } to mirror the existing MessageNotFound pattern, short-circuits authed_json on status_code == 401 before report_error fires, and logs at info with the same structured fields. Two new tests cover the happy path (two paths/methods to prove status-driven suppression) and the 403 regression guard. The irrefutable letlet...else conversions in existing tests are a required mechanical fix since the enum is no longer single-variant.

The fix is correct. 403 continuing to report is the right call — that's a scope/permission bug, not a token-as-a-whole rejection. The failure = "non_2xx" field in the tracing log is a minor label oddity (the whole point is this isn't a failure), but it mirrors the 404 block and is harmless — not worth changing here.

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

CI went green — approving.

The 401 short-circuit is tight and correct. Mirrors the MessageNotFound pattern exactly: typed error variant, structured tracing::info!, skips report_error, 403 untouched. The let...else conversions in the tests are the right mechanical fix for the now-multi-variant enum. Two new tests cover what they need to — status-driven (not path-keyed) suppression, plus the regression guard on 403. Nothing to change here.

@graycyrus graycyrus merged commit 1a404ac into tinyhumansai:main May 28, 2026
34 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants