Skip to content

fix(auth): narrow SessionExpired to confirmed OpenHuman backend 401s#2356

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/session-expired-cascade-2286
May 21, 2026
Merged

fix(auth): narrow SessionExpired to confirmed OpenHuman backend 401s#2356
senamakel merged 3 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/session-expired-cascade-2286

Conversation

@M3gA-Mind
Copy link
Copy Markdown
Contributor

@M3gA-Mind M3gA-Mind commented May 20, 2026

Summary

  • Narrows is_session_expired_error in src/core/jsonrpc.rs so DomainEvent::SessionExpired only fires for confirmed OpenHuman session expiry, not for downstream provider 401s.
  • Adds is_downstream_provider_auth_error helper for diagnostic logging only (no session side-effects).
  • Adds 'provider_auth' error kind to CoreRpcErrorKind in coreRpcClient.ts; tightens classifyRpcError with the same HTTP-method-prefix logic.
  • Fixes Discord card-click logout (issue Discord channel card click logs the user out #2285) as a direct consequence.

Root Cause

is_session_expired_error used a loose "401 + unauthorized" string match. Discord bot-token failures arrive as "Discord API error: Discord list guilds failed (401): Unauthorized" — which contains both "401" and "unauthorized" — causing the full user session to be cleared on every Discord card interaction.

Fix

OpenHuman backend errors (from authed_json in src/api/rest.rs) always use the format "{HTTP_METHOD} /path failed (401 Unauthorized): {body}". Provider errors start with the provider name. The fix keeps the "401 + unauthorized" branch only when the message starts with an HTTP method verb, which matches backend paths while excluding Discord, OpenAI, Anthropic, Composio, etc.

Test plan

  • src/core/jsonrpc_tests.rs — 10 is_session_expired_error tests covering: HTTP-method-prefix matches, Discord exclusion, BYO-key exclusion, Composio exclusion, explicit markers still match
  • app/src/services/__tests__/coreRpcClient.test.ts — 3 new test.each rows: Discord/OpenAI/Anthropic 401 → provider_auth; existing GET /teams failed (401 Unauthorized)auth_expired preserved
  • cargo test -p openhuman is_session_expired — 10/10 pass
  • pnpm test:coverage — full Vitest suite pass
  • pnpm compile + cargo check — clean
  • pnpm format:check — clean

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case)
  • Diff coverage ≥ 80% — all new/changed lines in jsonrpc.rs and coreRpcClient.ts covered by unit tests
  • No new external network dependencies introduced
  • N/A: Coverage matrix — no new production feature rows
  • N/A: Manual smoke checklist — no release surfaces touched

Related

Closes #2286
Related: #2285 (Discord card-click logout — fixed as a consequence of this change)


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/session-expired-cascade-2286
  • Commit SHA: a0da242

Validation Run

  • pnpm --filter openhuman-app compile
  • pnpm --filter openhuman-app format:check
  • pnpm --filter openhuman-app lint
  • cargo check --manifest-path Cargo.toml
  • pnpm test:coverage (Vitest full suite)
  • cargo test -p openhuman is_session_expired (10/10 pass)

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: provider-auth 401s (Discord, OpenAI BYO-key, Composio direct-mode) no longer clear the user session
  • User-visible effect: clicking the Discord channel card no longer logs the user out; BYO-key misconfiguration no longer forces re-auth

Parity Contract

  • Legacy behavior preserved: OpenHuman backend 401s (GET /teams failed (401 Unauthorized)) still trigger session expiry
  • Guard/fallback/dispatch parity checks: api_error in inference/provider/ops.rs still publishes SessionExpired directly for backend auth failures (independent of this fix)

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this PR

Summary by CodeRabbit

  • Bug Fixes

    • Improved error classification to distinguish user session expiry from external provider authentication failures, reducing mistaken session terminations and improving recovery and logging behavior.
  • Tests

    • Expanded and tightened test coverage to ensure confirmed session-expiry signals are detected while external API 401/unauthorized responses do not trigger session-expiry handling.

Review Change Stack

…inyhumansai#2286)

Previously, `is_session_expired_error` fired on any error containing
"401 + unauthorized", causing Discord bot-token failures, BYO-key
provider 401s, and Composio direct-mode errors to clear the user's
app session and force re-authentication.

The fix distinguishes error origins by format:
- OpenHuman backend errors (via `authed_json`) use "{METHOD} /path
  failed (401 Unauthorized): {body}" — they start with an HTTP verb.
- Provider errors ("Discord API error: ...", "OpenAI API error ...")
  start with a provider name, not an HTTP method.

Changes:
- `is_session_expired_error`: keeps explicit session markers ("session
  expired", SESSION_EXPIRED, "no backend session token", "session jwt
  required") and the HTTP-method-prefixed 401 check; removes the
  bare "invalid token" and generic "401 + unauthorized" matches.
- Adds `is_downstream_provider_auth_error` helper for diagnostic
  logging only (no side effects).
- `coreRpcClient.ts`: adds `provider_auth` error kind; tightens
  `classifyRpcError` to match backend-path 401s by HTTP-method prefix
  and route remaining 401s to `provider_auth` instead of `auth_expired`.
- Tests updated in `jsonrpc_tests.rs` and `coreRpcClient.test.ts`.

Closes tinyhumansai#2286
@M3gA-Mind M3gA-Mind requested a review from a team May 20, 2026 14:57
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Caution

Review failed

Failed to post review comments

📝 Walkthrough

Walkthrough

This PR narrows SessionExpired publication to confirmed OpenHuman user-session expiry by introducing a provider_auth error kind, tightening session-expiry detection to require explicit markers or HTTP-method-prefixed backend 401s, and logging downstream provider authorization failures instead of clearing the session.

Changes

Session Expiry vs. Provider Auth Distinction

Layer / File(s) Summary
Error kind and RPC classification contract
app/src/services/coreRpcClient.ts
CoreRpcErrorKind exports a new provider_auth variant to distinguish downstream provider/integration 401 failures from user-session expiry.
TypeScript RPC error classifier and tests
app/src/services/coreRpcClient.ts, app/src/services/__tests__/coreRpcClient.test.ts
classifyRpcError expands regex/marker checks to separate explicit backend/session-expiry signals from downstream provider 401s and invalid-token patterns, returning provider_auth for provider errors; tests assert Discord, OpenAI, and Anthropic 401s classify as provider_auth.
Rust session-expiry predicate
src/core/jsonrpc.rs
is_session_expired_error rewritten to match only explicit OpenHuman session markers via observability helper or HTTP-method-prefixed backend 401s; diagnostic-only predicate remains for logging.
invoke_method integration and logging
src/core/jsonrpc.rs
invoke_method publishes SessionExpired only when is_session_expired_error confirms expiry and now logs non-expiry provider 401s at info level without triggering session teardown.
Test coverage for session-expiry and provider-auth separation
src/core/jsonrpc_tests.rs
Tests now require HTTP-method-prefixed backend 401 matching for session expiry, add positive OpenHuman backend 401 cases, and add regression negatives for Discord and BYO-key provider 401s; "invalid token" no longer matches session-expiry.

Sequence Diagram(s)

sequenceDiagram
  participant invoke_method
  participant is_session_expired_error
  participant is_downstream_provider_auth_error
  participant SessionExpiredEvent

  invoke_method->>is_session_expired_error: check error text
  is_session_expired_error-->>invoke_method: true (explicit session expiry)
  invoke_method->>SessionExpiredEvent: publish SessionExpired (sanitized reason)
  invoke_method->>is_downstream_provider_auth_error: else check provider 401 patterns
  is_downstream_provider_auth_error-->>invoke_method: true (provider 401)
  invoke_method-->>invoke_method: log info (provider auth, no teardown)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Possibly related issues

  • #2285 — Similar objective to prevent downstream 401s (e.g., Discord card) from triggering SessionExpired and logging out users.

Suggested reviewers

  • senamakel
  • graycyrus

Poem

🐰 I nibble logs and chase the clue,
A 401 from Discord stays true—
Provider slips, the session stays,
I hop and patch the graceful way. 🥕

🚥 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 title clearly and accurately summarizes the main objective: narrowing SessionExpired classification to only confirmed OpenHuman backend 401s, which is the primary focus of this PR.
Linked Issues check ✅ Passed The PR fully addresses all coding objectives from issue #2286: narrows SessionExpired to confirmed OpenHuman backend 401s, adds provider_auth classification, tightens HTTP-method-prefix detection, excludes downstream provider 401s, and includes comprehensive test coverage meeting diff coverage ≥80%.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objectives: error classification narrowing in Rust and TypeScript, test additions, and helper function additions directly supporting the SessionExpired fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 20, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/core/jsonrpc.rs (1)

196-201: 💤 Low value

Consider sanitizing the provider auth failure log message.

The session-expired path (line 193) sanitizes the error message via sanitize_api_error, but the provider auth log on line 200 logs msg directly. While provider error messages typically don't contain secrets, they can include user-supplied paths or tokens that were rejected. For consistency with the session-expired path, consider sanitizing:

         } else if is_downstream_provider_auth_error(msg) {
+            let redacted = crate::openhuman::inference::provider::ops::sanitize_api_error(msg);
             log::info!(
                 "[jsonrpc] downstream provider auth failure for method='{}' (not session expiry) — {}",
                 method,
-                msg
+                redacted
             );
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/jsonrpc.rs` around lines 196 - 201, The provider auth failure log in
the is_downstream_provider_auth_error branch currently logs msg directly; change
the log to use the same sanitizer as the session-expired path by passing the
error through sanitize_api_error before logging (i.e., use
sanitize_api_error(msg) in the log::info call), keeping the log message and
method variable unchanged so provider errors are consistently sanitized.
app/src/services/coreRpcClient.ts (1)

111-138: 💤 Low value

Verify regex alignment with Rust is_session_expired_error.

The TypeScript regex on line 126 uses ^(GET|POST|PUT|DELETE|PATCH)\s+\/[^\s].*\(401\b.*Unauthorized\)/i while the Rust implementation uses starts_with("GET /") etc. with separate contains("401") and contains("unauthorized") checks.

The TypeScript regex requires:

  • \(401\b.*Unauthorized\) — both "401" and "Unauthorized" must appear with "401" followed by a word boundary and eventually "Unauthorized" (in any order relative to other text, but both present)

The Rust version checks:

  • msg.starts_with("GET /") (note: no space between method and slash)
  • lower.contains("401") && lower.contains("unauthorized")

The TypeScript regex expects GET /path (space then slash) while Rust expects GET / (method immediately followed by space-slash). Both should work for the actual backend format "GET /teams failed (401 Unauthorized)", but the subtle difference in the regex pattern is worth noting for future maintenance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/services/coreRpcClient.ts` around lines 111 - 138, The HTTP-verb 401
check in coreRpcClient.ts (the if using the regex
/^(GET|POST|PUT|DELETE|PATCH)\s+\/[^\s].*\(401\b.*Unauthorized\)/i) is stricter
than the Rust is_session_expired_error logic; update the condition so it only
requires the message to start with an HTTP verb + space + '/' and to contain
both "401" and "unauthorized" (case-insensitive) rather than requiring the
specific "(401 ... Unauthorized)" sequence—i.e., replace the single complex
regex check on message with a starts-with-verb check plus separate contains
checks for "401" and "unauthorized" (use the same message variable and keep the
same verb list GET/POST/PUT/DELETE/PATCH).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/src/services/coreRpcClient.ts`:
- Around line 111-138: The HTTP-verb 401 check in coreRpcClient.ts (the if using
the regex /^(GET|POST|PUT|DELETE|PATCH)\s+\/[^\s].*\(401\b.*Unauthorized\)/i) is
stricter than the Rust is_session_expired_error logic; update the condition so
it only requires the message to start with an HTTP verb + space + '/' and to
contain both "401" and "unauthorized" (case-insensitive) rather than requiring
the specific "(401 ... Unauthorized)" sequence—i.e., replace the single complex
regex check on message with a starts-with-verb check plus separate contains
checks for "401" and "unauthorized" (use the same message variable and keep the
same verb list GET/POST/PUT/DELETE/PATCH).

In `@src/core/jsonrpc.rs`:
- Around line 196-201: The provider auth failure log in the
is_downstream_provider_auth_error branch currently logs msg directly; change the
log to use the same sanitizer as the session-expired path by passing the error
through sanitize_api_error before logging (i.e., use sanitize_api_error(msg) in
the log::info call), keeping the log message and method variable unchanged so
provider errors are consistently sanitized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84a6abb7-3377-40d9-9d0a-6b23352bd735

📥 Commits

Reviewing files that changed from the base of the PR and between cc498d1 and a0da242.

📒 Files selected for processing (4)
  • app/src/services/__tests__/coreRpcClient.test.ts
  • app/src/services/coreRpcClient.ts
  • src/core/jsonrpc.rs
  • src/core/jsonrpc_tests.rs

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 20, 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.

Review — fix(auth): narrow SessionExpired to confirmed OpenHuman backend 401s

Solid, well-scoped fix for a high-severity session-stability bug. The root cause analysis is excellent — the old is_session_expired_error matched any "401 + unauthorized" substring, which meant provider-originating 401s (Discord bot tokens, BYO-key OpenAI/Anthropic, Composio) were nuking the user's session. The fix correctly uses the HTTP-method prefix (GET /, POST /, etc.) as a discriminator since authed_json in rest.rs always formats errors with that prefix while provider errors start with the provider name.

What's good

  • Clean separation: explicit markers (Session expired, SESSION_EXPIRED, no backend session token, session jwt required) checked first, then the HTTP-method-prefix 401 path.
  • is_downstream_provider_auth_error is properly scoped to diagnostic logging only — no side effects.
  • The else if at the invoke_method call site means backend 401s never double-log as provider errors.
  • Rust and TS classification logic mirror each other correctly.
  • 10 Rust tests + 3 new TS test rows covering the key cases (backend 401, Discord, BYO-key, Composio).
  • All CI green, including coverage gate.

Issue alignment

  • #2286 — all acceptance criteria met: session expiry narrowed, downstream 401s typed as recoverable, no token wipe on unrelated 401, logging improved with method/source context, Discord regression protected, tests comprehensive.
  • #2285 — fixed as a consequence (Discord card-click no longer triggers session expiry).
Area Files Verdict
Rust core jsonrpc.rs, jsonrpc_tests.rs Clean
Frontend coreRpcClient.ts, coreRpcClient.test.ts Clean

One minor note inline. No critical or major issues found.

Comment thread src/core/jsonrpc.rs
…nyhumansai#2356

- Sanitize provider auth error msg via sanitize_api_error before logging
  (addresses @coderabbitai on src/core/jsonrpc.rs:196-201)
- Add comment noting HEAD/OPTIONS intentionally excluded from HTTP method
  allowlist (addresses @graycyrus on src/core/jsonrpc.rs:219)
- Align TS classifyRpcError HTTP-verb 401 check with Rust is_session_expired_error:
  use starts-with-verb check + separate /401/ and /unauthorized/i tests
  instead of strict /(401.*Unauthorized)/ regex (addresses @coderabbitai on
  app/src/services/coreRpcClient.ts:111-138)
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 20, 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.

Continuation review — all prior feedback addressed.

The follow-up commit adds the HEAD/OPTIONS scope comment I flagged, and also hardens the downstream provider auth log by redacting through sanitize_api_error — good improvement.

The regex restructuring in coreRpcClient.ts (splitting the single pattern into three separate checks aligned with the Rust side) is cleaner and easier to reason about.

No new issues found. Previous [minor] resolved. This PR is ready for merge.

# Conflicts:
#	src/core/jsonrpc.rs
#	src/core/jsonrpc_tests.rs
@senamakel senamakel merged commit c81fe3d into tinyhumansai:main May 21, 2026
29 checks passed
@senamakel
Copy link
Copy Markdown
Member

huge thanks @M3gA-Mind 🙌 love how surgical this fix is, scoping sessionexpired to only confirmed openhuman 401s (and keeping provider auth stuff as diagnostic-only) is exactly the kind of nuance that saves so much debugging pain later. always a treat seeing your prs land 🚀

@fzamel3333-ai
Copy link
Copy Markdown

Summary

  • Narrows is_session_expired_error in src/core/jsonrpc.rs so DomainEvent::SessionExpired only fires for confirmed OpenHuman session expiry, not for downstream provider 401s.
  • Adds is_downstream_provider_auth_error helper for diagnostic logging only (no session side-effects).
  • Adds 'provider_auth' error kind to CoreRpcErrorKind in coreRpcClient.ts; tightens classifyRpcError with the same HTTP-method-prefix logic.
  • Fixes Discord card-click logout (issue Discord channel card click logs the user out #2285) as a direct consequence.

Root Cause

is_session_expired_error used a loose "401 + unauthorized" string match. Discord bot-token failures arrive as "Discord API error: Discord list guilds failed (401): Unauthorized" — which contains both "401" and "unauthorized" — causing the full user session to be cleared on every Discord card interaction.

Fix

OpenHuman backend errors (from authed_json in src/api/rest.rs) always use the format "{HTTP_METHOD} /path failed (401 Unauthorized): {body}". Provider errors start with the provider name. The fix keeps the "401 + unauthorized" branch only when the message starts with an HTTP method verb, which matches backend paths while excluding Discord, OpenAI, Anthropic, Composio, etc.

Test plan

  • src/core/jsonrpc_tests.rs — 10 is_session_expired_error tests covering: HTTP-method-prefix matches, Discord exclusion, BYO-key exclusion, Composio exclusion, explicit markers still match
  • app/src/services/__tests__/coreRpcClient.test.ts — 3 new test.each rows: Discord/OpenAI/Anthropic 401 → provider_auth; existing GET /teams failed (401 Unauthorized)auth_expired preserved
  • cargo test -p openhuman is_session_expired — 10/10 pass
  • pnpm test:coverage — full Vitest suite pass
  • pnpm compile + cargo check — clean
  • pnpm format:check — clean

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case)
  • Diff coverage ≥ 80% — all new/changed lines in jsonrpc.rs and coreRpcClient.ts covered by unit tests
  • No new external network dependencies introduced
  • N/A: Coverage matrix — no new production feature rows
  • N/A: Manual smoke checklist — no release surfaces touched

Related

Closes #2286
Related: #2285 (Discord card-click logout — fixed as a consequence of this change)


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/session-expired-cascade-2286
  • Commit SHA: a0da242

Validation Run

  • pnpm --filter openhuman-app compile
  • pnpm --filter openhuman-app format:check
  • pnpm --filter openhuman-app lint
  • cargo check --manifest-path Cargo.toml
  • pnpm test:coverage (Vitest full suite)
  • cargo test -p openhuman is_session_expired (10/10 pass)

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: provider-auth 401s (Discord, OpenAI BYO-key, Composio direct-mode) no longer clear the user session
  • User-visible effect: clicking the Discord channel card no longer logs the user out; BYO-key misconfiguration no longer forces re-auth

Parity Contract

  • Legacy behavior preserved: OpenHuman backend 401s (GET /teams failed (401 Unauthorized)) still trigger session expiry
  • Guard/fallback/dispatch parity checks: api_error in inference/provider/ops.rs still publishes SessionExpired directly for backend auth failures (independent of this fix)

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this PR

Summary by CodeRabbit

  • Bug Fixes

    • Improved error classification to distinguish user session expiry from external provider authentication failures, reducing mistaken session terminations and improving recovery and logging behavior.
  • Tests

    • Expanded and tightened test coverage to ensure confirmed session-expiry signals are detected while external API 401/unauthorized responses do not trigger session-expiry handling.

Review Change Stack

Auto runn

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.

SessionExpired clears the session for unrelated backend 401s

4 participants