mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-12 15:19:16 +03:00
fix(mcp): clean up failed HTTP connections
This commit is contained in:
@@ -5,10 +5,13 @@ import asyncio
|
||||
import socket
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from nanobot.agent.tools import mcp as mcp_mod
|
||||
from nanobot.agent.tools.mcp import _probe_http_url, connect_mcp_servers
|
||||
from nanobot.agent.tools.registry import ToolRegistry
|
||||
from nanobot.config.schema import MCPServerConfig
|
||||
from nanobot.security.network import configure_ssrf_whitelist
|
||||
|
||||
_PROXY_ENV_VARS = ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy")
|
||||
@@ -171,6 +174,58 @@ async def test_connect_skips_unreachable_sse():
|
||||
assert len(registry._tools) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_isolates_streamable_http_status_failure(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""A reachable endpoint returning HTTP 530 must not poison the event loop."""
|
||||
async def _reachable(_url: str) -> bool:
|
||||
return True
|
||||
|
||||
def _return_http_530(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(530, text="cloudflare error 1033", request=request)
|
||||
|
||||
monkeypatch.setattr(mcp_mod, "validate_url_target", lambda _url: (True, ""))
|
||||
monkeypatch.setattr(mcp_mod, "_probe_http_url", _reachable)
|
||||
monkeypatch.setattr(
|
||||
mcp_mod,
|
||||
"PinnedDNSAsyncTransport",
|
||||
lambda: httpx.MockTransport(_return_http_530),
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
previous_exception_handler = loop.get_exception_handler()
|
||||
unhandled: list[BaseException] = []
|
||||
|
||||
def _capture_unhandled(_loop: asyncio.AbstractEventLoop, context: dict) -> None:
|
||||
if isinstance(context.get("exception"), BaseException):
|
||||
unhandled.append(context["exception"])
|
||||
|
||||
loop.set_exception_handler(_capture_unhandled)
|
||||
try:
|
||||
registry = ToolRegistry()
|
||||
stacks = await asyncio.wait_for(
|
||||
connect_mcp_servers(
|
||||
{
|
||||
"cloudflare": MCPServerConfig(
|
||||
type="streamableHttp",
|
||||
url="https://mcp.example.com/mcp",
|
||||
)
|
||||
},
|
||||
registry,
|
||||
),
|
||||
timeout=5.0,
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert stacks == {}
|
||||
assert registry.tool_names == []
|
||||
assert unhandled == []
|
||||
assert not any(task.get_name() == "mcp:cloudflare" for task in asyncio.all_tasks())
|
||||
finally:
|
||||
loop.set_exception_handler(previous_exception_handler)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_probe_not_called_for_stdio():
|
||||
"""stdio transport should not be probed — it spawns a local process."""
|
||||
|
||||
Reference in New Issue
Block a user