Skip to content

Commit af8c597

Browse files
committed
fix: remove stale no-cover pragma and simplify regression test
The integration test now covers the Exception branch in _receive_loop, so the pragma is no longer valid (caught by strict-no-cover on CI). Also simplified the regression test: zero-buffer write stream + no concurrent receiver makes it deterministic — send_log_message() would block on send(), then raise ClosedResourceError when _receive_loop exits and closes the stream. No concurrent task, no uncovered branches, no sleeps.
1 parent c1fe08c commit af8c597

File tree

2 files changed

+21
-16
lines changed

2 files changed

+21
-16
lines changed

src/mcp/shared/session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ async def _receive_loop(self) -> None:
334334
async with self._read_stream, self._write_stream:
335335
try:
336336
async for message in self._read_stream:
337-
if isinstance(message, Exception): # pragma: no cover
337+
if isinstance(message, Exception):
338338
await self._handle_incoming(message)
339339
elif isinstance(message.message, JSONRPCRequest):
340340
try:

tests/server/test_lowlevel_exception_handling.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,25 @@ async def test_server_run_exits_cleanly_when_transport_yields_exception_then_clo
8282
"""
8383
server = Server("test-server")
8484

85-
read_send, read_recv = anyio.create_memory_object_stream[SessionMessage | Exception](10)
86-
write_send, write_recv = anyio.create_memory_object_stream[SessionMessage](10)
87-
88-
async def transport_side() -> None:
89-
# What the streamable HTTP transport does: push the exception, then close.
90-
await read_send.send(RuntimeError("simulated transport error"))
91-
await read_send.aclose()
92-
# Drain the write side so it doesn't back-pressure; it must see no messages.
93-
async with write_recv:
94-
async for _ in write_recv: # pragma: no cover
95-
pytest.fail("server must not write back when the transport reported an error")
85+
read_send, read_recv = anyio.create_memory_object_stream[SessionMessage | Exception](1)
86+
# Zero-buffer on the write stream forces send() to block until received.
87+
# With no receiver, a send() sits blocked until _receive_loop exits its
88+
# `async with self._read_stream, self._write_stream:` block and closes the
89+
# stream, at which point the blocked send raises ClosedResourceError.
90+
# This deterministically reproduces the race without sleeps.
91+
write_send, write_recv = anyio.create_memory_object_stream[SessionMessage](0)
92+
93+
# What the streamable HTTP transport does: push the exception, then close.
94+
read_send.send_nowait(RuntimeError("simulated transport error"))
95+
read_send.close()
9696

9797
with anyio.fail_after(5):
98-
async with anyio.create_task_group() as tg:
99-
tg.start_soon(transport_side)
100-
# stateless=True so server.run doesn't wait for initialize handshake
101-
await server.run(read_recv, write_send, server.create_initialization_options(), stateless=True)
98+
# stateless=True so server.run doesn't wait for initialize handshake.
99+
# Before this fix, this raised ExceptionGroup(ClosedResourceError).
100+
await server.run(read_recv, write_send, server.create_initialization_options(), stateless=True)
101+
102+
# write_send was closed inside _receive_loop's `async with`; receive_nowait
103+
# raises EndOfStream iff the buffer is empty (i.e., server wrote nothing).
104+
with pytest.raises(anyio.EndOfStream):
105+
write_recv.receive_nowait()
106+
write_recv.close()

0 commit comments

Comments
 (0)