Skip to content

Commit 5071aab

Browse files
committed
fix(sse): Handle ClosedResourceError during shutdown
Improved the fix for the cancel scope lifecycle violation by adding graceful handling of ClosedResourceError. When streams close during shutdown, the error handler may attempt to send exceptions to already closed streams, which should be ignored as this is expected behavior. This maintains the original fix (removing manual tg.cancel_scope.cancel()) while handling the race condition it exposed in error cleanup.
1 parent 49ee0f9 commit 5071aab

File tree

1 file changed

+11
-6
lines changed

1 file changed

+11
-6
lines changed

src/mcp/client/sse.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ async def sse_reader(
111111
raise sse_exc
112112
except Exception as exc:
113113
logger.exception("Error in sse_reader")
114-
await read_stream_writer.send(exc)
114+
try:
115+
await read_stream_writer.send(exc)
116+
except anyio.ClosedResourceError:
117+
# Stream already closed during shutdown, which is expected
118+
logger.debug("Stream closed during error handling - ignoring")
115119
finally:
116120
await read_stream_writer.aclose()
117121

@@ -142,11 +146,12 @@ async def post_writer(endpoint_url: str):
142146
try:
143147
yield read_stream, write_stream
144148
finally:
145-
# FIX: Removed manual cancel - anyio task group handles cleanup automatically
146-
# The manual cancel caused: "RuntimeError: Attempted to exit cancel scope
147-
# in a different task than it was entered in"
148-
# When the async context manager exits, the task group's __aexit__
149-
# will properly cancel all child tasks.
149+
# FIX: Removed manual tg.cancel_scope.cancel() that violated anyio lifecycle
150+
# The original code called tg.cancel_scope.cancel() here, but this caused:
151+
# "RuntimeError: Attempted to exit cancel scope in a different task than it was entered in"
152+
#
153+
# The task group's __aexit__ will properly cancel all child tasks when
154+
# this context manager exits, so no manual cancellation is needed.
150155
pass
151156
finally:
152157
await read_stream_writer.aclose()

0 commit comments

Comments
 (0)