Gracefully terminate active sessions during StreamableHTTP shutdown#2259
Open
Bortlesboat wants to merge 2 commits intomodelcontextprotocol:mainfrom
Open
Gracefully terminate active sessions during StreamableHTTP shutdown#2259Bortlesboat wants to merge 2 commits intomodelcontextprotocol:mainfrom
Bortlesboat wants to merge 2 commits intomodelcontextprotocol:mainfrom
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2150
Problem
When shutting down a StreamableHTTP server while clients have active streaming connections, Uvicorn logs:
This happens because
StreamableHTTPSessionManager.run()cancels its task group on shutdown without first terminating active sessions. TheEventSourceResponsecoroutines are killed mid-stream before they can complete their HTTP responses.There are two compounding issues:
run()never callsterminate()on active transports before cancelling the task group — it just clears the dict.terminate()itself doesn't close_sse_stream_writers. It closes_request_streams, but the SSE writers that keepEventSourceResponsealive are stored separately and never touched during termination.Fix
streamable_http_manager.py— Inrun()'sfinallyblock, iterate over all active transports and callterminate()on each before cancelling the task group.streamable_http.py— Interminate():_sse_stream_writersso activeEventSourceResponseinstances complete gracefully.terminate()may now be called from both the manager shutdown path and the per-session cleanup path.