fix(backend_api): suppress Sentry on 401 via typed BackendApiError::Unauthorized#2781
Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR introduces explicit HTTP 401 Unauthorized error classification in the REST API layer. The backend HTTP status 401 now surfaces as a distinct ChangesHTTP 401 Unauthorized classification
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
graycyrus
left a comment
There was a problem hiding this comment.
@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 let → let...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.
graycyrus
left a comment
There was a problem hiding this comment.
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.
Summary
401 Unauthorizedon anyauthed_jsoncall 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_jsoncurrently funnels all non-2xx (except a narrow set of 404/transient/budget cases) intoreport_error, so each lapsed session generates a Sentry event per authed call until the user re-signs-in.BackendApiError::MessageNotFound(404) pattern: addBackendApiError::Unauthorized { method, path }, short-circuitstatus_code == 401beforereport_error, log atinfowith the same structured fields, and return the typed error so callers can route to a re-sign-in flow.Problem
Sentry OPENHUMAN-TAURI-4K8 —
POST /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 becausesynthesizeSpeechautoplays 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:BackendApiError::Unauthorized { method: String, path: String }variant with the same authoring intent asMessageNotFound(docs reference the Sentry short ID).authed_json, immediately before the existing 404 block:err.to_string().contains("401")onauthed_jsonerrors (verified via grep acrosssrc/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): ...".BackendApiError::MessageNotFoundlet-bindings inrest_tests.rsare now refutable (multi-variant enum); converted tolet ... else { panic!(...) }. No production code change required (bus.rsalready usesif let Some(...)).Submission Checklist
authed_json_surfaces_unauthorized_on_401(both/openai/v1/audio/speechand/referral/statsto prove status-driven, not path-keyed) +authed_json_403_is_not_demoted_to_unauthorized(regression guard: only 401 → Unauthorized, 403 still reports).rest.rsis exercised by the new tests (the 401 branch, the typed error construction, the structured log fields).N/A: classification refinement on an existing path; no feature row added/removed/renamed.Closes #NNN—N/A: surfaced from Sentry, no GitHub tracking issue filed.Impact
authed_jsonis the shared backend API helper, so every authed RPC benefits."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
BackendApiError::MessageNotFound(OPENHUMAN-TAURI-2Y).Unauthorizedinto 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
fix/backend-api-401-typed-errora48875c1Validation Run
cargo test --lib -p openhuman authed_json— 5/5 pass (3 existing + 2 new).cargo test --lib -p openhuman channels::bus::— 9/9 pass (no downstreamMessageNotFounddowncast 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) andcargo check --manifest-path app/src-tauri/Cargo.tomlerror:worktree lacksnode_modules(nopnpm 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 inCLAUDE.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 (noapp/orapp/src-tauri/files touched).Behavior Changes
authed_jsonreturns typedBackendApiError::Unauthorizedand is logged atinfoinstead of being reported to Sentry. All other status codes unchanged.Parity Contract
MessageNotFound404 behavior unchanged;report_erroris still called for every other non-2xx that does not match an explicit suppression.Summary by CodeRabbit