fix(api): suppress expected backend 401s from Sentry via typed-error flatten (#3297)#3336
fix(api): suppress expected backend 401s from Sentry via typed-error flatten (#3297)#3336oxoxDev wants to merge 4 commits into
Conversation
…on-expiry sentinel (tinyhumansai#3297)
…piry classification (tinyhumansai#3297)
📝 WalkthroughWalkthroughThis PR adds a ChangesSession-Expired Error Flattening
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
no this is not the way to handle bugs. we need to RCA and patch it. |
Summary
BackendApiError::Unauthorized(Display"backend rejected session token on {method} {path}"), but the team/billing ops flatten it to aStringbefore the JSON-RPC dispatcher — and that string matches none of the session-expiry classifiers, so expected session-lapse 401s leak to Sentry.api::flatten_authed_error, which downcasts the typed error and mapsUnauthorizedonto the existingSESSION_EXPIREDsentinel (every other error keeps its full{e:#}chain).team::opsandbilling::opsauthed_jsonerrors through the helper./teams/me/usage, 32 users) and TAURI-RUST-8WZ (/payments/stripe/currentPlan, 27 users), and the rest of the authed-endpoint family, without a brittle global string match.Problem
BackendApiError::Unauthorizedis an expected user-session state (token expired / revoked / rotated server-side), not a code bug — the REST layer intentionally skipsreport_errorfor it. But callers flatten it to aString(format!("{e:#}")/e.to_string()) before it reaches the JSON-RPC dispatcher (jsonrpc.rs). There, all three session-expiry classifiers —is_session_expired_error(jsonrpc.rs),is_session_expired_message(observability.rs), and thebefore_sendnetis_session_expired_event(observability.rs) — still match the old"401" + "unauthorized" + "GET /"wording. The new Display string matches none, so expected session-lapse 401s flow to Sentry on everyauthed_jsoncaller once a session lapses.Background: #2924 added a matching arm for
"backend rejected session token", but #2959 deliberately reverted all string-based Sentry suppression. So re-adding a global prose arm is the wrong direction.Solution
Fix it at the ops boundary — the last place the typed error still exists and is downcastable:
api::flatten_authed_error(err: anyhow::Error) -> String:downcast_ref::<BackendApiError>(); onUnauthorized { method, path }returns"SESSION_EXPIRED: backend rejected session token on {method} {path}".MessageNotFound) → unchangedformat!("{e:#}").SESSION_EXPIREDsentinel is already recognised byis_session_expired_message, so the dispatcher both suppresses the Sentry report and publishesDomainEvent::SessionExpired(auth domain drives re-sign-in) — no new behavior wiring needed.team::opsandbilling::opsroute theirauthed_jsonerrors through the helper (single seam → endpoint-agnostic for current and future callers).Design decision: the mapping keys off the typed downcast, not the Display wording, so it stays correct if the
#[error(...)]text changes. The sentinel is minted deterministically from the type — not a match on upstream prose — which is consistent with #2959's intent (kill brittle string matches), unlike the reverted #2924 arm. A fully-typed end-to-end channel (changing the RPCResult<Value, String>error type) was considered and rejected as too large for this fix.Submission Checklist
.map_err(flatten_authed_error)seams in team/billing ops are exercised through the helper's own unit tests); enforced by the Rust Core Coverage CI gate.N/A: behaviour-only change(no feature added/removed/renamed; internal error-routing refinement).## Related—N/A: behaviour-only change.N/A: no release-cut surface touched (internal Sentry error routing).Closes #NNNin the## RelatedsectionImpact
authed_jsonendpoints stop reporting as errors and instead drive the existingSessionExpiredre-auth path.MessageNotFound) — they keep their full anyhow chain and still reach Sentry.Related
Sentry-Issue: TAURI-RUST-8WY
Sentry-Issue: TAURI-RUST-8WZ
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
fix/3297-backend-401-sentry-suppressc585a3239bcc932a96238e58e3e488d3f60e1cccValidation Run
N/A: no frontend changes—pnpm --filter openhuman-app format:checkN/A: no frontend changes—pnpm typecheckcargo test --lib -- api::rest::key_bytes_from_string_tests::flatten core::jsonrpc::tests::is_session_expired_error_matches_flattened_backend_unauthorized→ 4 passedcargo fmt+cargo check(exit 0) +cargo clippy --lib(exit 0, no new warnings)N/A: Tauri shell untouched— Tauri fmt/checkValidation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
team/billingauthed_jsoncalls are classified as session expiry (suppressed from Sentry, driveSessionExpired).Parity Contract
{e:#}chain and still report;MessageNotFoundnot swallowed; 403 still reported.flatten_authed_error_preserves_non_unauthorized_chainandflatten_authed_error_does_not_swallow_message_not_found.Duplicate / Superseded PR Handling
Summary by CodeRabbit
Bug Fixes
Tests