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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions