Skip to content

fix(client): reconnect backends automatically after they lose their session - #414

Open
Quentin-M wants to merge 2 commits into
1mcp-app:mainfrom
Quentin-M:fix/reconnect-on-backend-session-loss
Open

fix(client): reconnect backends automatically after they lose their session#414
Quentin-M wants to merge 2 commits into
1mcp-app:mainfrom
Quentin-M:fix/reconnect-on-backend-session-loss

Conversation

@Quentin-M

@Quentin-M Quentin-M commented Jul 30, 2026

Copy link
Copy Markdown

Problem

When a downstream MCP server restarts, its in-memory Streamable HTTP / SSE session store resets, invalidating any session ID 1MCP was holding for it. ClientManager's client.onerror handler only logs the error:

client.onerror = (error) => {
  logger.error(`Client ${name} error: ${error}`);
};

It never recovers. Every subsequent request to that backend — tools/list during capability aggregation and tools/call — keeps failing with the server's "Session not found" response until the whole 1MCP process is restarted by hand. In practice this means a single backend restart silently breaks that backend for every client of the proxy until someone notices and bounces 1MCP.

Reproduced this against a real deployment (1MCP fronting several backends, one Streamable HTTP, one SSE): restarting a backend container reliably produced repeating Client <name> error: ... "Session not found" (Streamable HTTP) / ... Could not find session ID '...' (SSE) log lines, with the backend's tools absent from tools/list and its tool calls failing, indefinitely, until 1MCP itself was restarted.

Fix

onerror now recognizes this error class (isSessionLostError, matching both message shapes above) and reconnects the backend through a freshly recreated transport with the stale sessionId dropped, so the new transport performs a full initialize handshake and is issued a new session instead of replaying the dead one.

TransportRecreator already had recreateHttpTransport, used for OAuth-retry recreation, which always carries the old sessionId forward (correct there — the session itself is still valid, only auth needs refreshing). This PR adds an opt-in { preserveSessionId: false } option plus a recreateForSessionLoss convenience wrapper for the session-loss case, without changing the existing OAuth-retry behavior/callers.

The existing client.client !== erroredClient guard pattern (already used in onclose) prevents a stale/superseded client instance from triggering a duplicate recovery, and createSingleClient's existing per-name semaphore dedupes any concurrent recovery attempts.

Testing

  • New unit tests in transportRecreator.test.ts covering preserveSessionId: false and recreateForSessionLoss.
  • New unit tests in clientManager.test.ts covering: recovery on the Streamable HTTP "Session not found" message, recovery on the SSE "Could not find session ID" message, and a negative case ensuring unrelated onerror errors don't trigger recovery.
  • pnpm test:unit (via vitest run) — full suite passes except 4 pre-existing failures unrelated to this change (verified identical on main without this diff): two release-notes-range.test.ts failures from git tag needing a configured commit identity in this sandbox, and two commonFormatters.test.ts date-formatting failures that are timezone-dependent.
  • tsc --noEmit and eslint clean.

Summary by CodeRabbit

  • Bug Fixes
    • Improved automatic recovery when Streamable HTTP/SSE sessions are lost or invalidated (client stays connected after recovery).
    • Reconnects with a fresh session after “session not found” errors.
    • Added safer handling so recovery errors won’t surface from the error callback, and the original transport remains unchanged when recovery can’t proceed.
    • Ensures recovered transports don’t reuse invalid session IDs, while allowing session ID preservation for HTTP recreation when appropriate.

…ession

When a downstream MCP server restarts, its in-memory Streamable HTTP / SSE
session store resets, invalidating any session ID 1MCP was holding for it.
The client's onerror handler only logged the resulting error and never
recovered, so every subsequent request to that backend — including tools/list
and tools/call — kept failing with "Session not found" until the whole 1MCP
process was restarted by hand.

onerror now detects this error class and reconnects the backend through a
freshly recreated transport with the stale sessionId dropped, so it performs
a full initialize handshake and gets a new session instead of replaying the
dead one. TransportRecreator gains an opt-in `preserveSessionId: false` mode
(and a `recreateForSessionLoss` convenience wrapper) for this; OAuth-retry
recreation is unaffected and keeps preserving the session ID by default.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cd91334f-417f-49a9-b218-775bc847d23f

📥 Commits

Reviewing files that changed from the base of the PR and between 1c60bdd and a118105.

📒 Files selected for processing (2)
  • src/core/client/clientManager.test.ts
  • src/core/client/clientManager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/client/clientManager.ts

📝 Walkthrough

Walkthrough

ClientManager now detects lost Streamable HTTP/SSE sessions, recreates transports without stale session IDs, handles recreation failures, and reconnects active clients. Tests cover supported and unrelated errors, transport replacement, connected status, and unsupported transports.

Changes

Session loss recovery

Layer / File(s) Summary
Session-aware transport recreation
src/core/client/transportRecreator.ts, src/core/client/transportRecreator.test.ts
Adds optional session-ID preservation and a session-loss recreation path that creates transports without the previous session ID.
Client session-loss recovery
src/core/client/clientManager.ts, src/core/client/clientManager.test.ts
Recognizes session-not-found errors, reconnects active clients with fresh transports, catches unsupported recreation failures, and ignores unrelated errors.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ClientManager
  participant TransportRecreator
  Client->>ClientManager: onerror(session lost)
  ClientManager->>TransportRecreator: recreateForSessionLoss(transport, serverName)
  TransportRecreator-->>ClientManager: fresh session-less transport
  ClientManager->>ClientManager: createSingleClient(fresh transport)
Loading

Suggested reviewers: xizhibei

Poem

A rabbit found a session gone,
Rebuilt the path at early dawn.
No stale ID hopped along,
A fresh client sang its song.
“ECONNRESET? Not this time!” 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: automatic client-side reconnection after backend session loss.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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.

Inline comments:
In `@src/core/client/clientManager.test.ts`:
- Around line 381-405: The SSE recovery test should exercise the actual SSE
transport and verify successful reconnection, not merely invocation of
recreateForSessionLoss. In the test around createSingleClient, construct the
fixture with SSEClientTransport, then await the transport replacement and assert
the client reaches ClientStatus.Connected while preserving the existing
session-loss error trigger.

In `@src/core/client/clientManager.ts`:
- Around line 205-208: Restrict the session-loss recovery flow in the client
manager around recreateForSessionLoss to HTTP/SSE transports before invoking it,
so unsupported AuthProviderTransport instances cannot throw from client.onerror;
alternatively catch recreation failures within that handler before
createSingleClient is scheduled. Add a regression test covering a non-HTTP
transport and confirming the error is safely handled without escaping the client
error path.

In `@src/core/client/transportRecreator.test.ts`:
- Around line 64-69: Replace manually constructed transport fixtures with the
established factories from test/unit-utils/MockFactories.ts. Update the stale
Streamable HTTP and session-loss fixtures in
src/core/client/transportRecreator.test.ts lines 64-69 and 79-84, and the
original/fresh, SSE recovery, and unrelated-error fixtures in
src/core/client/clientManager.test.ts lines 339-349, 384-388, and 410-414,
preserving each fixture’s required transport type and state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 050c8294-e73a-4a26-83a7-a8e392e0796b

📥 Commits

Reviewing files that changed from the base of the PR and between f3b1281 and 1c60bdd.

📒 Files selected for processing (4)
  • src/core/client/clientManager.test.ts
  • src/core/client/clientManager.ts
  • src/core/client/transportRecreator.test.ts
  • src/core/client/transportRecreator.ts

Comment thread src/core/client/clientManager.test.ts
Comment thread src/core/client/clientManager.ts
Comment thread src/core/client/transportRecreator.test.ts
- Guard recoverFromSessionLoss's transport recreation with a try/catch:
  recreateForSessionLoss throws for non-HTTP/SSE transports, and that throw
  was previously outside the createSingleClient(...).catch() path, so it
  could escape client.onerror uncaught for any backend whose transport isn't
  HTTP/SSE. Added a regression test with a stdio-style transport.
- Fix the SSE recovery test: it was prototyping its fixture as
  StreamableHTTPClientTransport instead of SSEClientTransport, and only
  asserted that recreateForSessionLoss was invoked rather than exercising
  the real SSE recreation path and confirming the client actually reaches
  ClientStatus.Connected on a replaced transport.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant