Skip to content

Sessions stuck in pre-init state have no timeout, leak indefinitely #808

@andrico21

Description

@andrico21

Describe the bug
rmcp 1.4.0
Sessions created by LocalSessionManager that never receive an initialize request remain alive indefinitely. The session_config.keep_alive timeout is only enforced inside LocalSessionWorker::run() after initialization - sessions waiting in the get initialize request phase have no timeout and are only cleaned up on server shutdown. In production, any HTTP POST to /mcp that creates a session but never follows up with initialize (e.g. a health probe, a load balancer preflight, a client that disconnects immediately, or a test harness bug) causes a permanent session leak. Over time this exhausts memory.

To Reproduce

  1. Start a StreamableHttpService with LocalSessionManager.
  2. Send an HTTP POST to /mcp with a valid JSON-RPC request that triggers session creation (e.g. initialize), but drop the TCP connection before sending the follow-up initialized notification - or send a non-initialize request that still creates a session
  3. Observe that the session remains in memory indefinitely
  4. Only server shutdown cleans it up

Expected behavior
Sessions waiting for initialize should have a configurable timeout (e.g. init_timeout, defaulting to 30-60 seconds). If no initialize request arrives within that window, the session should be terminated.

Logs
Server running with session_config.keep_alive = 1200s (20 min). Four sessions were created at 10:54:10 that never received initialize. They remained alive for 48 minutes until the server was shut down at 11:42:22, at which point they terminated with:

ERROR rmcp::transport::worker: worker quit with fatal: transport terminated, when get initialize request

Meanwhile, sessions that did initialize were correctly reaped after 20 minutes of inactivity.

Additional context
Add a timeout in the pre-init phase of LocalSessionWorker, something like:

let init_timeout = self.session_config.init_timeout.unwrap_or(Duration::from_secs(60));
tokio::select! {
    req = self.wait_for_initialize() => { /* proceed */ },
    _ = tokio::time::sleep(init_timeout) => {
        return Err(WorkerQuitReason::fatal(
            LocalSessionWorkerError::InitTimeout(init_timeout),
            "get initialize request"
        ))
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is not working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions