fix: handle HTTP error status codes in streamable HTTP client (#2047)
This commit is contained in:
committed by
GitHub
parent
8f669a77e3
commit
2fe56e56de
@@ -19,6 +19,7 @@ from mcp.client._transport import TransportStreams
|
||||
from mcp.shared._httpx_utils import create_mcp_http_client
|
||||
from mcp.shared.message import ClientMessageMetadata, SessionMessage
|
||||
from mcp.types import (
|
||||
INTERNAL_ERROR,
|
||||
INVALID_REQUEST,
|
||||
PARSE_ERROR,
|
||||
ErrorData,
|
||||
@@ -273,7 +274,13 @@ class StreamableHTTPTransport:
|
||||
await ctx.read_stream_writer.send(session_message)
|
||||
return
|
||||
|
||||
response.raise_for_status()
|
||||
if response.status_code >= 400:
|
||||
if isinstance(message, JSONRPCRequest):
|
||||
error_data = ErrorData(code=INTERNAL_ERROR, message="Server returned an error response")
|
||||
session_message = SessionMessage(JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data))
|
||||
await ctx.read_stream_writer.send(session_message)
|
||||
return
|
||||
|
||||
if is_initialization:
|
||||
self._maybe_extract_session_id_from_response(response)
|
||||
|
||||
|
||||
@@ -116,6 +116,58 @@ async def test_unexpected_content_type_sends_jsonrpc_error() -> None:
|
||||
await session.list_tools()
|
||||
|
||||
|
||||
def _create_http_error_app(error_status: int, *, error_on_notifications: bool = False) -> Starlette:
|
||||
"""Create a server that returns an HTTP error for non-init requests."""
|
||||
|
||||
async def handle_mcp_request(request: Request) -> Response:
|
||||
body = await request.body()
|
||||
data = json.loads(body)
|
||||
|
||||
if data.get("method") == "initialize":
|
||||
return _init_json_response(data)
|
||||
|
||||
if "id" not in data:
|
||||
if error_on_notifications:
|
||||
return Response(status_code=error_status)
|
||||
return Response(status_code=202)
|
||||
|
||||
return Response(status_code=error_status)
|
||||
|
||||
return Starlette(debug=True, routes=[Route("/mcp", handle_mcp_request, methods=["POST"])])
|
||||
|
||||
|
||||
async def test_http_error_status_sends_jsonrpc_error() -> None:
|
||||
"""Verify HTTP 5xx errors unblock the pending request with an MCPError.
|
||||
|
||||
When a server returns a non-2xx status code (e.g. 500), the client should
|
||||
send a JSONRPCError so the pending request resolves immediately instead of
|
||||
raising an unhandled httpx.HTTPStatusError that causes the caller to hang.
|
||||
"""
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=_create_http_error_app(500))) as client:
|
||||
async with streamable_http_client("http://localhost/mcp", http_client=client) as (read_stream, write_stream):
|
||||
async with ClientSession(read_stream, write_stream) as session: # pragma: no branch
|
||||
await session.initialize()
|
||||
|
||||
with pytest.raises(MCPError, match="Server returned an error response"): # pragma: no branch
|
||||
await session.list_tools()
|
||||
|
||||
|
||||
async def test_http_error_on_notification_does_not_hang() -> None:
|
||||
"""Verify HTTP errors on notifications are silently ignored.
|
||||
|
||||
When a notification gets an HTTP error, there is no pending request to
|
||||
unblock, so the client should just return without sending a JSONRPCError.
|
||||
"""
|
||||
app = _create_http_error_app(500, error_on_notifications=True)
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app)) as client:
|
||||
async with streamable_http_client("http://localhost/mcp", http_client=client) as (read_stream, write_stream):
|
||||
async with ClientSession(read_stream, write_stream) as session: # pragma: no branch
|
||||
await session.initialize()
|
||||
|
||||
# Should not raise or hang — the error is silently ignored for notifications
|
||||
await session.send_notification(RootsListChangedNotification(method="notifications/roots/list_changed"))
|
||||
|
||||
|
||||
def _create_invalid_json_response_app() -> Starlette:
|
||||
"""Create a server that returns invalid JSON for requests."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user