Skip to content

Commit 30ff4a5

Browse files
authored
fix: recursively extract HTTP errors from nested ExceptionGroups (#3556)
1 parent 16d16ec commit 30ff4a5

2 files changed

Lines changed: 43 additions & 6 deletions

File tree

src/agents/mcp/server.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -721,13 +721,12 @@ def _extract_http_error_from_exception(self, e: BaseException) -> Exception | No
721721
if isinstance(e, httpx.HTTPStatusError | httpx.ConnectError | httpx.TimeoutException):
722722
return e
723723

724-
# Check if it's an ExceptionGroup containing HTTP errors
724+
# Recursively check ExceptionGroups for HTTP errors
725725
if isinstance(e, BaseExceptionGroup):
726726
for exc in e.exceptions:
727-
if isinstance(
728-
exc, httpx.HTTPStatusError | httpx.ConnectError | httpx.TimeoutException
729-
):
730-
return exc
727+
result = self._extract_http_error_from_exception(exc)
728+
if result is not None:
729+
return result
731730

732731
return None
733732

tests/mcp/test_server_errors.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1+
import builtins
2+
import sys
3+
from unittest.mock import MagicMock, patch
4+
5+
import httpx
16
import pytest
27

38
from agents import Agent
49
from agents.exceptions import UserError
5-
from agents.mcp.server import _MCPServerWithClientSession
10+
from agents.mcp.server import MCPServerStreamableHttp, _MCPServerWithClientSession
611
from agents.run_context import RunContextWrapper
712

13+
# Handle Python version compatibility for ExceptionGroups
14+
if sys.version_info < (3, 11):
15+
from exceptiongroup import BaseExceptionGroup
16+
else:
17+
BaseExceptionGroup = builtins.BaseExceptionGroup
18+
819

920
class CrashingClientSessionServer(_MCPServerWithClientSession):
1021
def __init__(self):
@@ -45,3 +56,30 @@ async def test_not_calling_connect_causes_error():
4556

4657
with pytest.raises(UserError):
4758
await server.call_tool("foo", {})
59+
60+
61+
@pytest.mark.asyncio
62+
async def test_call_tool_nested_exception_group_mapping():
63+
"""
64+
Regression test ensuring that nested ExceptionGroups containing HTTP errors
65+
are recursively extracted and mapped to a UserError in call_tool().
66+
"""
67+
# 1. Initialize the server with mock streamable parameters
68+
server = MCPServerStreamableHttp(params={"url": "http://fake-mcp-server"})
69+
70+
# 2. Simulate an active connection by mocking the session object
71+
server.session = MagicMock()
72+
73+
# 3. Construct a nested ExceptionGroup hierarchy containing a connection error
74+
http_error = httpx.ConnectError("Network unreachable")
75+
inner_group = BaseExceptionGroup("inner_failures", [http_error])
76+
outer_group = BaseExceptionGroup("outer_failures", [inner_group])
77+
78+
# 4 & 5. Mock the internal retry handler to raise the nested group, and assert UserError
79+
with patch.object(server, "_call_tool_with_isolated_retry", side_effect=outer_group):
80+
with pytest.raises(UserError) as exc_info:
81+
await server.call_tool(tool_name="test_tool", arguments={})
82+
83+
# 6. Verify that the user-facing message is mapped correctly based on the root cause
84+
assert "Connection lost" in str(exc_info.value)
85+
assert exc_info.value.__cause__ is http_error

0 commit comments

Comments
 (0)