Skip to content

Request handlers not cancelled when transport connection closes unexpectedly #611

Open
@aramesh7

Description

@aramesh7

Problem Description

When a client connection closes unexpectedly (network failure, timeout, crash), the server continues executing in-flight request handlers instead of cancelling them. This leads to resource waste and prevents proper cleanup in distributed/production environments.

Current Behavior

  1. Client times out and closes connection
  2. Server's Protocol._onclose() is called
  3. Response handlers are cleaned up
  4. ⚠️ Request handlers continue running until completion
  5. Server resources (CPU, memory, external API calls) are wasted

Expected Behavior

  1. Client times out and closes connection
  2. Server's Protocol._onclose() is called
  3. Response handlers are cleaned up
  4. Request handlers are cancelled via their AbortSignals
  5. Tools/handlers can clean up immediately

Root Cause

In src/shared/protocol.ts, the _onclose() method cleans up response handlers but does not abort request handlers:

_onclose() {
    var _a;
    const responseHandlers = this._responseHandlers;
    this._responseHandlers = new Map();
    this._progressHandlers.clear();
    
    // ❌ Missing: Abort in-flight request handlers
    // for (const controller of this._requestHandlerAbortControllers.values()) {
    //     controller.abort(new McpError(ErrorCode.ConnectionClosed, "Connection closed"));
    // }
    // this._requestHandlerAbortControllers.clear();
    
    this._transport = undefined;
    (_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
    const error = new McpError(ErrorCode.ConnectionClosed, "Connection closed");
    for (const handler of responseHandlers.values()) {
        handler(error);
    }
}

Impact

  • Single-instance servers: Request handlers waste resources after client disconnects
  • Distributed servers: No way to stop requests when server instances restart/crash
  • Long-running operations: Continue unnecessarily (file uploads, database operations, etc.)
  • Resource leaks: External API calls, file handles, network connections remain open

Proposed Solution

Add request handler cancellation to Protocol._onclose():

_onclose() {
    var _a;
    const responseHandlers = this._responseHandlers;
    this._responseHandlers = new Map();
    this._progressHandlers.clear();
    
    // ✅ Add: Abort all in-flight request handlers
    for (const controller of this._requestHandlerAbortControllers.values()) {
        controller.abort(new McpError(ErrorCode.ConnectionClosed, "Connection closed"));
    }
    this._requestHandlerAbortControllers.clear();
    
    this._transport = undefined;
    (_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
    const error = new McpError(ErrorCode.ConnectionClosed, "Connection closed");
    for (const handler of responseHandlers.values()) {
        handler(error);
    }
}

Why This Matters

While clients do attempt to send explicit notifications/cancelled messages on timeout, these can fail due to:

  • Network already broken
  • Server already crashed
  • Connection already closed
  • Transport errors

Connection closure should serve as an implicit cancellation signal, just like how response handlers are cleaned up.

Verification

The infrastructure is already in place:

  • _requestHandlerAbortControllers Map exists
  • ✅ AbortControllers are created per request in _onrequest()
  • ✅ Request handlers receive extra.signal for cancellation
  • ✅ Transport layers call _onclose() on connection drops

This is simply connecting existing cancellation infrastructure to connection close events.

Files Affected

  • src/shared/protocol.ts (lines ~83-97 in _onclose() method)

Environment:

  • MCP SDK Version: 1.12.1
  • Transport: StreamableHTTP, SSE, Stdio (affects all)

This issue affects production deployments where proper resource cleanup on unexpected disconnections is critical for system stability.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions