|
8 | 8 | import multiprocessing |
9 | 9 | import socket |
10 | 10 | import time |
11 | | -from collections.abc import Generator |
| 11 | +from collections.abc import AsyncIterator, Generator |
12 | 12 | from datetime import timedelta |
13 | 13 | from typing import Any |
14 | 14 | from unittest.mock import MagicMock |
@@ -2543,3 +2543,39 @@ def handler(request: httpx.Request) -> httpx.Response: |
2543 | 2543 | assert reply.message.root.error == types.ErrorData( |
2544 | 2544 | code=types.INTERNAL_ERROR, message="Server returned an error response" |
2545 | 2545 | ) |
| 2546 | + |
| 2547 | + |
| 2548 | +@pytest.mark.anyio |
| 2549 | +async def test_a_non_2xx_body_that_fails_to_read_still_resolves_the_caller() -> None: |
| 2550 | + """A stalled or truncated error body must not strand the caller. |
| 2551 | +
|
| 2552 | + `aread()` on the non-2xx body raises `ReadTimeout`/`RemoteProtocolError` when the connection |
| 2553 | + dies mid-body. Those derive from `httpx.HTTPError`, not `httpx.StreamError`, so letting the |
| 2554 | + body-parsing `except` miss them would put the escaping exception right back into the POST's |
| 2555 | + background task — the very bug this path exists to fix. |
| 2556 | + """ |
| 2557 | + |
| 2558 | + class _DyingStream(httpx.AsyncByteStream): |
| 2559 | + async def __aiter__(self) -> AsyncIterator[bytes]: |
| 2560 | + raise httpx.ReadTimeout("connection died mid-body") |
| 2561 | + yield b"" # pragma: no cover |
| 2562 | + |
| 2563 | + def handler(request: httpx.Request) -> httpx.Response: |
| 2564 | + return httpx.Response(500, headers={"content-type": "application/json"}, stream=_DyingStream()) |
| 2565 | + |
| 2566 | + with anyio.fail_after(5): |
| 2567 | + async with ( |
| 2568 | + httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http_client, |
| 2569 | + streamable_http_client("http://test/mcp", http_client=http_client) as (read, write, _), |
| 2570 | + read, |
| 2571 | + write, |
| 2572 | + ): |
| 2573 | + request = types.JSONRPCRequest(jsonrpc="2.0", id=1, method="tools/list", params={}) |
| 2574 | + await write.send(SessionMessage(types.JSONRPCMessage(request))) |
| 2575 | + reply = await read.receive() |
| 2576 | + |
| 2577 | + assert isinstance(reply, SessionMessage) |
| 2578 | + assert isinstance(reply.message.root, types.JSONRPCError) |
| 2579 | + assert reply.message.root.error == types.ErrorData( |
| 2580 | + code=types.INTERNAL_ERROR, message="Server returned an error response" |
| 2581 | + ) |
0 commit comments