Skip to content

Commit 2e0e00b

Browse files
wiggzzclaude
andcommitted
Move terminate() after task group exit for deterministic cancellation
Cancel the request-scoped task group first so the Cancelled exception deterministically reaches the server task before terminate() closes the streams. This avoids a race between Cancelled and ClosedResourceError in the message router. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f6909a1 commit 2e0e00b

File tree

2 files changed

+10
-4
lines changed

2 files changed

+10
-4
lines changed

src/mcp/server/streamable_http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1026,7 +1026,7 @@ async def message_router():
10261026
)
10271027
except anyio.ClosedResourceError:
10281028
if self._terminated:
1029-
logger.debug("Read stream closed by client") # pragma: no cover
1029+
logger.debug("Read stream closed by client") # pragma: lax no cover
10301030
else:
10311031
logger.exception("Unexpected closure of read stream in message router")
10321032
except Exception: # pragma: lax no cover

src/mcp/server/streamable_http_manager.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,14 +191,20 @@ async def run_request_handler(*, task_status: TaskStatus[None] = anyio.TASK_STAT
191191
task_status.started()
192192
# Handle the HTTP request and return the response
193193
await http_transport.handle_request(scope, receive, send)
194-
# Terminate the transport after the request is handled
195-
await http_transport.terminate()
196-
# Cancel the request-scoped task group to stop the server task
194+
# Cancel the request-scoped task group to stop the server task.
195+
# This ensures the Cancelled exception reaches the server task
196+
# before terminate() closes the streams, avoiding a race between
197+
# Cancelled and ClosedResourceError in the message router.
197198
request_tg.cancel_scope.cancel()
198199

199200
await request_tg.start(run_stateless_server)
200201
await request_tg.start(run_request_handler)
201202

203+
# Terminate after the task group exits — the server task is already
204+
# cancelled at this point, so this is just cleanup (sets _terminated
205+
# flag and closes any remaining streams).
206+
await http_transport.terminate()
207+
202208
async def _handle_stateful_request(self, scope: Scope, receive: Receive, send: Send) -> None:
203209
"""Process request in stateful mode - maintaining session state between requests."""
204210
request = Request(scope, receive)

0 commit comments

Comments
 (0)