Fail fast on server-to-client requests in JSON-response mode instead of hanging (#3195)
This commit is contained in:
parent
27f5cc7a46
commit
528e366558
1 changed files with 259 additions and 93 deletions
|
|
@ -25,6 +25,25 @@ class _PrimingFailingStore(EventStore):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
class _AsgiPost:
|
||||
"""A one-shot POST driven straight at `handle_request`, capturing what the transport sends."""
|
||||
|
||||
def __init__(self, body: bytes, headers: list[tuple[bytes, bytes]]) -> None:
|
||||
self.scope: Scope = {"type": "http", "method": "POST", "path": "/", "query_string": b"", "headers": headers}
|
||||
self.sent: list[Message] = []
|
||||
self._body = body
|
||||
self._body_sent = False
|
||||
|
||||
async def receive(self) -> Message:
|
||||
if not self._body_sent:
|
||||
self._body_sent = True
|
||||
return {"type": "http.request", "body": self._body, "more_body": False}
|
||||
raise NotImplementedError
|
||||
|
||||
async def send(self, message: Message) -> None:
|
||||
self.sent.append(message)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_router_unconsumed_request_stream_does_not_block_siblings() -> None:
|
||||
"""A response whose `sse_writer` is not yet receiving must not park the router (#1764).
|
||||
|
|
@ -73,35 +92,18 @@ async def test_priming_store_failure_leaves_no_per_request_state() -> None:
|
|||
event_store=_PrimingFailingStore(),
|
||||
)
|
||||
|
||||
body = b'{"jsonrpc":"2.0","id":"req-1","method":"tools/list","params":{}}'
|
||||
scope: Scope = {
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": "/",
|
||||
"query_string": b"",
|
||||
"headers": [
|
||||
post = _AsgiPost(
|
||||
b'{"jsonrpc":"2.0","id":"req-1","method":"tools/list","params":{}}',
|
||||
[
|
||||
(b"accept", b"application/json, text/event-stream"),
|
||||
(b"content-type", b"application/json"),
|
||||
(b"mcp-protocol-version", b"2025-11-25"),
|
||||
],
|
||||
}
|
||||
body_sent = False
|
||||
|
||||
async def receive() -> Message:
|
||||
nonlocal body_sent
|
||||
if not body_sent:
|
||||
body_sent = True
|
||||
return {"type": "http.request", "body": body, "more_body": False}
|
||||
raise NotImplementedError
|
||||
|
||||
sent: list[Message] = []
|
||||
|
||||
async def asgi_send(message: Message) -> None:
|
||||
sent.append(message)
|
||||
)
|
||||
|
||||
async with transport.connect() as (read_stream, _write_stream):
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(transport.handle_request, scope, receive, asgi_send)
|
||||
tg.start_soon(transport.handle_request, post.scope, post.receive, post.send)
|
||||
with anyio.fail_after(5):
|
||||
forwarded = await read_stream.receive()
|
||||
assert isinstance(forwarded, Exception)
|
||||
|
|
@ -110,7 +112,32 @@ async def test_priming_store_failure_leaves_no_per_request_state() -> None:
|
|||
assert transport._request_streams == {}
|
||||
assert transport._sse_stream_writers == {}
|
||||
|
||||
assert sent[0]["type"] == "http.response.start"
|
||||
assert sent[0]["status"] == 500
|
||||
body = b"".join(m.get("body", b"") for m in sent if m["type"] == "http.response.body")
|
||||
assert post.sent[0]["type"] == "http.response.start"
|
||||
assert post.sent[0]["status"] == 500
|
||||
body = b"".join(m.get("body", b"") for m in post.sent if m["type"] == "http.response.body")
|
||||
assert b"backend unavailable" not in body
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_json_post_answers_500_when_session_terminates_mid_request() -> None:
|
||||
"""A JSON-mode POST whose session is torn down before the handler answers gets a 500, not a stall."""
|
||||
transport = StreamableHTTPServerTransport(mcp_session_id="sid", is_json_response_enabled=True)
|
||||
post = _AsgiPost(
|
||||
b'{"jsonrpc":"2.0","id":"req-1","method":"tools/list","params":{}}',
|
||||
[
|
||||
(b"accept", b"application/json"),
|
||||
(b"content-type", b"application/json"),
|
||||
(b"mcp-session-id", b"sid"),
|
||||
(b"mcp-protocol-version", b"2025-11-25"),
|
||||
],
|
||||
)
|
||||
|
||||
async with transport.connect() as (read_stream, _write_stream):
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(transport.handle_request, post.scope, post.receive, post.send)
|
||||
with anyio.fail_after(5):
|
||||
await read_stream.receive() # the request reached the session; the POST is parked
|
||||
await transport.terminate()
|
||||
|
||||
assert post.sent[0]["type"] == "http.response.start"
|
||||
assert post.sent[0]["status"] == 500
|
||||
|
|
|
|||
Loading…
Reference in a new issue