{"record":{"id":"de031258cc2533a0","repo":"tinyhumansai/openhuman","slug":"session-expired-backend-session-token-expired-loc","errorCode":null,"errorMessage":"SESSION_EXPIRED: backend session token expired locally — re-authentication required","messagePattern":"SESSION_EXPIRED: backend session token expired locally — re-authentication required","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/openhuman/inference/provider/openhuman_backend_model.rs","lineNumber":139,"sourceCode":"            self.options.auth_profile_override.as_deref(),\n        )?;\n\n        // #5503: precheck the recorded JWT `exp` BEFORE building a request, the\n        // same way `require_live_session_token` guards the backend REST callers.\n        // Managed inference used to fire a doomed request on an expired-but-\n        // stored token and let the 401 come back — but an expired session can\n        // also surface upstream as a misleading \"model unavailable\", which is a\n        // core symptom of #5503 (all tiers \"die\" over a long session). Failing\n        // fast as `session_expired` routes the user to re-auth instead. Offline\n        // / local sessions (`is_local_session_token`) and `exp`-less tokens\n        // carry no recorded expiry, so `classify_session_token` returns `Live`\n        // for them — their behaviour is unchanged and the post-call 401 net\n        // still covers a server-side revocation.\n        match classify_session_token(profile.as_ref(), chrono::Utc::now()) {\n            SessionTokenCheck::Live(token) => Ok(token),\n            SessionTokenCheck::Expired => {\n                maybe_publish_local_session_expiry();\n                anyhow::bail!(\n                    \"SESSION_EXPIRED: backend session token expired locally — re-authentication required\"\n                )\n            }\n            SessionTokenCheck::Absent => {\n                anyhow::bail!(\"No backend session: store a JWT via auth (app-session)\")\n            }\n        }\n    }\n\n    fn base_url(&self) -> String {\n        format!(\n            \"{}/openai/v1\",\n            effective_api_url(&self.api_url).trim_end_matches('/')\n        )\n    }\n\n    /// Resolve the current JWT + base URL and build a fresh crate `OpenAiModel`\n    /// (Bearer). Rebuilt per call because the session JWT rotates.","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/inference/provider/openhuman_backend_model.rs#L121-L157","documentation":"Pre-flight JWT expiry check (#5503): before building a managed-backend request, `classify_session_token(profile, now)` compares the stored app-session JWT's recorded `exp` against the current clock. On `Expired`, the core publishes a local session-expiry event (`maybe_publish_local_session_expiry`) and bails with `SESSION_EXPIRED` — routing the user to re-auth instead of surfacing a misleading 'model unavailable' error. Offline/local and exp-less tokens classify as Live and are unaffected; server-side revocation is still caught by the post-call 401 net.","triggerScenarios":"A managed-backend inference call when the stored JWT's `exp` is in the past — typically a desktop session running longer than the token lifetime (the #5503 'all tiers die over a long session' symptom).","commonSituations":"Long-lived desktop sessions whose token expired without a refresh; machine clock skew making a valid token appear expired; token refresh flow failed silently earlier.","solutions":["Re-authenticate (sign in again) to mint a fresh JWT, then retry.","If it recurs frequently, verify the token refresh path is working (check auth logs) — tokens should refresh before expiring.","Check system clock correctness on the host; a fast/skewed clock prematurely expires tokens.","Restart the core after re-auth so cached auth state is rebuilt."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"use crate::openhuman::security::credentials::session_support::{classify_session_token, SessionTokenCheck};\nlet profile = auth.get_profile(APP_SESSION_PROVIDER, None)?;\nif !matches!(classify_session_token(profile.as_ref(), chrono::Utc::now()), SessionTokenCheck::Live(_)) {\n    return trigger_re_auth(); // expired or absent — don't fire the request\n}","typeGuard":"fn session_live(profile: Option<&AuthProfile>) -> bool {\n    matches!(classify_session_token(profile, chrono::Utc::now()), SessionTokenCheck::Live(_))\n}","tryCatchPattern":"match model.chat(req).await {\n    Err(e) if e.to_string().contains(\"expired locally\") => prompt_re_authentication(),\n    other => other,\n}","preventionTips":["Keep token refresh working so `exp` never passes while in use (the #5503 long-session failure).","Verify host clock sync — skew prematurely expires JWTs.","Handle the local session-expiry event in the UI to prompt re-auth proactively."],"tags":["session-expired","jwt","expiry","managed-backend"],"backgroundTag":"jwt-token-expired","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}