fix(fetch_url): surface network errors as 400 with detail
Previously, when httpx.get raised an HTTPError (ConnectError for host unreachable, ReadTimeout for slow servers, RemoteProtocolError, etc.) the exception bubbled up through the route handler as a bare 500 "Internal Server Error". The LLM got no information about what actually went wrong — could not tell whether the host was down, the port was closed, DNS failed, TLS handshake broke, or the request timed out. The only thing the agent could do was guess. Wrap the redirect loop in try/except for httpx.HTTPError and translate to ValueError. The existing exception handler in server.py turns ValueError into HTTP 400 with the message in the response detail, so the LLM now sees e.g.: fetch_url could not reach 'http://nm01.prod.internal:8042/': ConnectError: Connection refused. Check that the URL is reachable from the MCP service, the host is in Connection.url_allowlist, and the connection's auth/SSL settings are correct. The original exception is chained via `raise ... from exc` so loguru still records the full traceback with the original type, and the `__cause__` attribute is set on the ValueError for programmatic inspection. Note: upstream HTTP 4xx/5xx responses (server replied, even with an error status) are NOT translated — the FetchUrlResult carries the status code and body so the LLM can read what the server actually said. This is the intentional contrast with the no-response-at-all case (which now has clear 400 detail). Tests (3 new in tests/unit/test_fetch_url.py): - test_fetch_url_raises_400_with_detail_on_connect_error ConnectError("Connection refused") -> ValueError with "ConnectError", "Connection refused", the URL, and __cause__ chained. - test_fetch_url_raises_400_with_detail_on_timeout ReadTimeout("Timed out reading") -> ValueError with "ReadTimeout", "Timed out reading", __cause__ chained. - test_fetch_url_returns_body_for_4xx_5xx_upstream Upstream 503 with body "Service Unavailable - try again later" -> FetchUrlResult(status_code=503, body=...). Proves the intentional contrast. Route description in server.py updated with a new **Errors** section explaining the two error paths (no response = 400 with detail, got a response = body returned). Tests: 401 passed (was 398, +3 net). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,13 +83,27 @@ def fetch_url(url: str, connection_name: str) -> FetchUrlResult:
|
||||
verify = config.verify_for_httpx()
|
||||
|
||||
for hop in range(_MAX_REDIRECTS + 1):
|
||||
resp = httpx.get(
|
||||
url,
|
||||
auth=auth,
|
||||
verify=verify,
|
||||
timeout=_REQUEST_TIMEOUT_SECONDS,
|
||||
follow_redirects=False,
|
||||
)
|
||||
try:
|
||||
resp = httpx.get(
|
||||
url,
|
||||
auth=auth,
|
||||
verify=verify,
|
||||
timeout=_REQUEST_TIMEOUT_SECONDS,
|
||||
follow_redirects=False,
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
# Network-level failure (no response received). Surface the
|
||||
# exception class + message so the LLM can act on it.
|
||||
# ValueError -> 400 via the existing handler in server.py.
|
||||
# The cause chain (`from exc`) preserves the original
|
||||
# exception for loguru.
|
||||
raise ValueError(
|
||||
f"fetch_url could not reach {url!r}: "
|
||||
f"{type(exc).__name__}: {exc}. "
|
||||
f"Check that the URL is reachable from the MCP service, "
|
||||
f"the host is in Connection.url_allowlist, and the "
|
||||
f"connection's auth/SSL settings are correct."
|
||||
) from exc
|
||||
if resp.status_code not in _REDIRECT_STATUSES:
|
||||
logger.info(
|
||||
f"fetch_url ok url={url} connection_name={connection_name} "
|
||||
|
||||
Reference in New Issue
Block a user