X Tutup
Skip to content

Gracefully terminate active sessions during StreamableHTTP shutdown#2259

Open
Bortlesboat wants to merge 2 commits intomodelcontextprotocol:mainfrom
Bortlesboat:fix/streamable-http-shutdown-graceful
Open

Gracefully terminate active sessions during StreamableHTTP shutdown#2259
Bortlesboat wants to merge 2 commits intomodelcontextprotocol:mainfrom
Bortlesboat:fix/streamable-http-shutdown-graceful

Conversation

@Bortlesboat
Copy link

Fixes #2150

Problem

When shutting down a StreamableHTTP server while clients have active streaming connections, Uvicorn logs:

ERROR: ASGI callable returned without completing response.

This happens because StreamableHTTPSessionManager.run() cancels its task group on shutdown without first terminating active sessions. The EventSourceResponse coroutines are killed mid-stream before they can complete their HTTP responses.

There are two compounding issues:

  1. run() never calls terminate() on active transports before cancelling the task group — it just clears the dict.
  2. terminate() itself doesn't close _sse_stream_writers. It closes _request_streams, but the SSE writers that keep EventSourceResponse alive are stored separately and never touched during termination.

Fix

streamable_http_manager.py — In run()'s finally block, iterate over all active transports and call terminate() on each before cancelling the task group.

streamable_http.py — In terminate():

  • Close all _sse_stream_writers so active EventSourceResponse instances complete gracefully.
  • Add an early-return guard for idempotency, since terminate() may now be called from both the manager shutdown path and the per-session cleanup path.

During server shutdown, StreamableHTTPSessionManager.run() cancelled
the task group without first terminating active sessions. This killed
EventSourceResponse coroutines mid-stream, causing Uvicorn to log
"ASGI callable returned without completing response" errors.

Two changes fix this:

1. In StreamableHTTPSessionManager.run(), iterate over all active
   transports and call terminate() on each before cancelling the
   task group.

2. In StreamableHTTPServerTransport.terminate(), close all SSE stream
   writers (_sse_stream_writers) so that active EventSourceResponse
   instances complete gracefully. Also add an early return guard for
   idempotency since terminate() may now be called from both the
   manager shutdown path and the per-session cleanup path.

Github-Issue: modelcontextprotocol#2150
Reported-by: emmahoggan
Cover the three lines that were missing from code coverage:

- streamable_http.py L777: terminate() closing active SSE stream writers
- streamable_http_manager.py L139-140: exception handling when
  transport.terminate() fails during manager shutdown

Github-Issue:modelcontextprotocol#2150
weiguangli-io added a commit to weiguangli-io/python-sdk that referenced this pull request Mar 10, 2026
Update the shutdown logic to wrap terminate() calls in try-except blocks,
preventing one transport's termination error from affecting others.

Changes:
- Use logger.exception() instead of logger.debug() for better error visibility
- Simplify SSE writer closing by iterating values() directly instead of pop()
- Improve code comments to explain why graceful termination is needed

This follows the same approach as PR modelcontextprotocol#2259 with minor improvements in
error handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.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.

Active Streamable HTTP sessions are not terminated during shutdown

1 participant

X Tutup