Skip to content

fix: await in-flight handlers before disposing to prevent ObjectDisposedException#1400

Open
jongio wants to merge 1 commit intomodelcontextprotocol:mainfrom
jongio:fix/1269-objectdisposedexception-tool-handler
Open

fix: await in-flight handlers before disposing to prevent ObjectDisposedException#1400
jongio wants to merge 1 commit intomodelcontextprotocol:mainfrom
jongio:fix/1269-objectdisposedexception-tool-handler

Conversation

@jongio
Copy link

@jongio jongio commented Feb 28, 2026

Summary

Fixes a race condition in stateless HTTP mode where ObjectDisposedException is thrown when an HTTP connection is aborted while tool handlers are still executing.

Closes #1269

Problem

In stateless mode, McpSessionHandler.ProcessMessagesCoreAsync fires and forgets message handler tasks (_ = ProcessMessageAsync()). When the HTTP connection is aborted (e.g., client timeout), the disposal chain runs:

  1. context.RequestAborted fires
  2. Session transport is disposed, completing the message channel
  3. ProcessMessagesCoreAsync exits the foreach loop
  4. Session and server are disposed
  5. ASP.NET Core disposes context.RequestServices (the HTTP request scope)

But in step 3, fire-and-forget handler tasks may still be running. When they try to access scoped services (like DbContext or ILogger), ObjectDisposedException is thrown because the request scope was disposed in step 5.

Fix

Track handler tasks in ProcessMessagesCoreAsync and await them in the finally block before allowing the method to return. Since ProcessMessagesCoreAsync is awaited indirectly by the HTTP request handler (via ServerRunTask), this keeps the request scope alive until all handlers complete:

  • Handler tasks are captured in a List<Task> instead of being discarded
  • The finally block calls Task.WhenAll(pendingHandlers) before failing pending requests
  • Exceptions from handlers are already logged individually in ProcessMessageAsync, so they are caught and suppressed in WhenAll

This is a minimal change to the core message processing loop that fixes the issue without changing the service provider or scope configuration.

Testing

  • Added regression test ScopedServices_AccessibleInToolHandler_AfterConnectionAbort that:
    • Registers a tool handler that blocks, waits for the connection to abort, then accesses scoped services
    • Verifies the scoped service is accessible without ObjectDisposedException after connection abort
    • Uses a side-channel (TaskCompletionSource) to verify handler outcome since the client call is aborted
  • All existing StatelessServerTests continue to pass
  • Full test suite: 303 passed, 11 skipped, 2 pre-existing conformance failures

…sedException

In stateless HTTP mode, fire-and-forget message handlers could outlive the
HTTP request scope. When a connection was aborted (e.g., client timeout),
ASP.NET Core disposed the request's IServiceProvider while tool handlers
were still executing, causing ObjectDisposedException when accessing scoped
services like DbContext.

Track handler tasks in ProcessMessagesCoreAsync and await them in the finally
block. Since ProcessMessagesCoreAsync is awaited by the session disposal chain
(which runs inside the HTTP request handler), this keeps the request scope
alive until all handlers complete.

Closes modelcontextprotocol#1269

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

ObjectDisposedException - IServiceProvider disposed while tool handler execution.

1 participant