fix(auth): treat missing backend session token as auth-expired (#1504)

This commit is contained in:
Steven Enamakel
2026-05-11 18:14:44 -07:00
committed by GitHub
parent 64e57e743e
commit 60a5c4f90f
4 changed files with 42 additions and 0 deletions
@@ -500,6 +500,13 @@ describe('classifyRpcError', () => {
['GET /teams failed (401 Unauthorized): {"success":false}', undefined, 'auth_expired'],
['Session expired. Please log in again.', undefined, 'auth_expired'],
['some prefix Session expired suffix', undefined, 'auth_expired'],
[
'composio unavailable: no backend session token. Sign in first (auth_store_session).',
undefined,
'auth_expired',
],
['no backend session token; run auth_store_session first', undefined, 'auth_expired'],
['NO BACKEND SESSION TOKEN', undefined, 'auth_expired'],
['HTTP 429 rate-limit exceeded', undefined, 'rate_limited'],
['Budget exceeded for current period', undefined, 'budget_exceeded'],
['Insufficient budget for request', undefined, 'budget_exceeded'],
+6
View File
@@ -77,6 +77,12 @@ export function classifyRpcError(message: string, httpStatus?: number): CoreRpcE
if (httpStatus === 401) return 'auth_expired';
if (httpStatus === 429) return 'rate_limited';
if (/\(401\b.*Unauthorized\)|Session expired/i.test(message)) return 'auth_expired';
// Core-side "no backend session token" → the auth profile is gone but the
// frontend may still hold a stale sessionToken from an optimistic post-login
// patch. Treat as auth-expired so `CoreStateProvider` clears the session and
// `ProtectedRoute` bounces the user back to `/` (login) instead of trapping
// them on an onboarding step that polls a failing RPC every 5 s.
if (/no backend session token/i.test(message)) return 'auth_expired';
if (/429.*rate.?limit/i.test(message)) return 'rate_limited';
if (/Budget exceeded|Insufficient budget/i.test(message)) return 'budget_exceeded';
if (/error sending request|client error \(Connect\)|timed out|ECONNREFUSED/i.test(message)) {
+10
View File
@@ -118,10 +118,20 @@ pub async fn invoke_method(state: AppState, method: &str, params: Value) -> Resu
}
/// Helper to determine if an error message indicates an expired or invalid session.
///
/// "No backend session token" is also treated as a session-expired signal: the
/// auth profile is missing entirely (the user was never signed in, or their
/// stored profile was wiped between login and the next RPC). The frontend may
/// still believe it holds a session token from an optimistic post-login patch,
/// so we want the same auto-cleanup + UI-level re-auth path to fire instead of
/// repeatedly reporting this as a hard error to Sentry. See #1465-ish: users
/// stuck on the onboarding `SkillsStep` would spam `composio_list_connections`
/// failures every 5 s without ever being bounced back to the login screen.
fn is_session_expired_error(msg: &str) -> bool {
let lower = msg.to_lowercase();
(lower.contains("401") && lower.contains("unauthorized"))
|| lower.contains("invalid token")
|| lower.contains("no backend session token")
|| msg.contains("SESSION_EXPIRED")
}
+19
View File
@@ -578,6 +578,25 @@ fn is_session_expired_error_does_not_match_unrelated_errors() {
assert!(!is_session_expired_error(""));
}
#[test]
fn is_session_expired_error_matches_missing_backend_session_token() {
// Composio / web search / billing / team / webhooks / referral all surface
// a "no backend session token" variant when the auth profile is gone. Each
// of these should funnel into the auto-cleanup path instead of being
// reported to Sentry as a fresh error on every 5 s poll.
assert!(is_session_expired_error(
"composio unavailable: no backend session token. Sign in first (auth_store_session)."
));
assert!(is_session_expired_error(
"no backend session token; run auth_store_session first"
));
assert!(is_session_expired_error(
"Web search unavailable: no backend session token. Sign in first so the server can proxy search."
));
// Case-insensitive match — the helper lowercases first.
assert!(is_session_expired_error("NO BACKEND SESSION TOKEN"));
}
#[test]
fn escape_html_escapes_all_special_chars() {
let raw = r#"<script>alert("x&y'z")</script>"#;