Skip to content

Commit 8243585

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 8ac87a9 commit 8243585

2 files changed

Lines changed: 10 additions & 4 deletions

File tree

src/mcp/server/streamable_http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1019,7 +1019,7 @@ async def message_router():
10191019
)
10201020
except anyio.ClosedResourceError:
10211021
if self._terminated:
1022-
logger.debug("Read stream closed by client") # pragma: no cover
1022+
logger.debug("Read stream closed by client") # pragma: lax no cover
10231023
else:
10241024
logger.exception("Unexpected closure of read stream in message router")
10251025
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)