-
Notifications
You must be signed in to change notification settings - Fork 3.2k
fix: propagate HTTP errors from transports instead of silently logging #2249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
642ca88
7605dfd
7d60257
715d47d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
|
|
||
| from mcp.client._transport import TransportStreams | ||
| from mcp.shared._httpx_utils import create_mcp_http_client | ||
| from mcp.shared.exceptions import HttpError | ||
| from mcp.shared.message import ClientMessageMetadata, SessionMessage | ||
| from mcp.types import ( | ||
| INTERNAL_ERROR, | ||
|
|
@@ -269,17 +270,41 @@ async def _handle_post_request(self, ctx: RequestContext) -> None: | |
|
|
||
| if response.status_code == 404: # pragma: no branch | ||
| if isinstance(message, JSONRPCRequest): # pragma: no branch | ||
| error_data = ErrorData(code=INVALID_REQUEST, message="Session terminated") | ||
| error_data = ErrorData( | ||
| code=INVALID_REQUEST, | ||
| message="Session terminated (HTTP 404)", | ||
| data={"http_status": 404}, | ||
| ) | ||
| session_message = SessionMessage(JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data)) | ||
| await ctx.read_stream_writer.send(session_message) | ||
| else: | ||
| raise HttpError(404, "Session terminated (HTTP 404)") | ||
| return | ||
|
|
||
| if response.status_code in (401, 403): | ||
| status_label = "Unauthorized" if response.status_code == 401 else "Forbidden" | ||
| error_message = f"HTTP {response.status_code} {status_label}" | ||
| if isinstance(message, JSONRPCRequest): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This raises after already sending a JSONRPCError to the read stream (line 26). But I think you either want to not raise here when the JSONRPCError was already sent, or skip the JSONRPCError and just raise (letting the wrapper forward it). The current approach of doing both will confuse callers. |
||
| error_data = ErrorData( | ||
| code=INTERNAL_ERROR, | ||
| message=error_message, | ||
| data={"http_status": response.status_code}, | ||
| ) | ||
| session_message = SessionMessage(JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data)) | ||
| await ctx.read_stream_writer.send(session_message) | ||
| raise HttpError(response.status_code, error_message) | ||
|
|
||
| if response.status_code >= 400: | ||
| error_message = f"HTTP {response.status_code}" | ||
| if isinstance(message, JSONRPCRequest): | ||
| error_data = ErrorData(code=INTERNAL_ERROR, message="Server returned an error response") | ||
| error_data = ErrorData( | ||
| code=INTERNAL_ERROR, | ||
| message=error_message, | ||
| data={"http_status": response.status_code}, | ||
| ) | ||
| session_message = SessionMessage(JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data)) | ||
| await ctx.read_stream_writer.send(session_message) | ||
| return | ||
| raise HttpError(response.status_code, error_message) | ||
|
|
||
| if is_initialization: | ||
| self._maybe_extract_session_id_from_response(response) | ||
|
|
@@ -467,19 +492,24 @@ async def post_writer( | |
| ) | ||
|
|
||
| async def handle_request_async(): | ||
| if is_resumption: | ||
| await self._handle_resumption_request(ctx) | ||
| else: | ||
| await self._handle_post_request(ctx) | ||
| try: | ||
| if is_resumption: | ||
| await self._handle_resumption_request(ctx) | ||
| else: | ||
| await self._handle_post_request(ctx) | ||
| except Exception as exc: | ||
| logger.exception("Error handling request") | ||
| await read_stream_writer.send(exc) | ||
|
|
||
| # If this is a request, start a new task to handle it | ||
| if isinstance(message, JSONRPCRequest): | ||
| tg.start_soon(handle_request_async) | ||
| else: | ||
| await handle_request_async() | ||
|
|
||
| except Exception: # pragma: lax no cover | ||
| except Exception as exc: # pragma: lax no cover | ||
| logger.exception("Error in post_writer") | ||
| await read_stream_writer.send(exc) | ||
| finally: | ||
| await read_stream_writer.aclose() | ||
| await write_stream.aclose() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-raising HttpError here means it escapes
post_writerentirely — nothing above catches it, so it propagates into the task group and crashes the whole SSE client connection. The original code intentionally swallowed all exceptions to keep the writer alive for the connection lifetime.For the SSE transport I think you want the same pattern as the other exceptions: send it through the read stream but don't re-raise. The caller can handle it from the stream without killing the connection.