fix: explain provider DNS failures as possible offline state
This commit is contained in:
parent
bdfdd2773f
commit
5f4a7e99f0
31
run_agent.py
31
run_agent.py
|
|
@ -2529,11 +2529,38 @@ class AIAgent:
|
|||
"""Extract a human-readable one-liner from an API error.
|
||||
|
||||
Handles Cloudflare HTML error pages (502, 503, etc.) by pulling the
|
||||
<title> tag instead of dumping raw HTML. Falls back to a truncated
|
||||
str(error) for everything else.
|
||||
<title> tag instead of dumping raw HTML. Network/DNS failures are
|
||||
translated into an offline hint, including when an SDK wraps the
|
||||
original OS error. Falls back to a truncated str(error) otherwise.
|
||||
"""
|
||||
raw = str(error)
|
||||
|
||||
# Linux, macOS, and Windows use different low-level messages when DNS
|
||||
# cannot resolve the provider while the device is offline. SDKs often
|
||||
# wrap that OSError in a generic "Connection error", so inspect the
|
||||
# exception chain before showing the top-level message to the user.
|
||||
network_resolution_markers = (
|
||||
"temporary failure in name resolution",
|
||||
"name or service not known",
|
||||
"nodename nor servname provided, or not known",
|
||||
"getaddrinfo failed",
|
||||
"no address associated with hostname",
|
||||
"network is unreachable",
|
||||
)
|
||||
current: Optional[BaseException] = error
|
||||
seen: set[int] = set()
|
||||
while current is not None and id(current) not in seen:
|
||||
seen.add(id(current))
|
||||
if any(
|
||||
marker in str(current).lower()
|
||||
for marker in network_resolution_markers
|
||||
):
|
||||
return (
|
||||
"Hermes can't reach the model provider. You may be offline. "
|
||||
"Check your internet connection and try again."
|
||||
)
|
||||
current = current.__cause__ or current.__context__
|
||||
|
||||
if (
|
||||
isinstance(error, ValueError)
|
||||
and "expected ident at line" in raw.lower()
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from types import SimpleNamespace
|
|||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from run_agent import AIAgent
|
||||
|
||||
|
|
@ -37,9 +38,42 @@ def test_empty_body_falls_back_to_response_json_error_message():
|
|||
assert "HTTP 400" in summary
|
||||
assert "model `foo` does not exist" in summary
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"technical_message",
|
||||
[
|
||||
"Temporary failure in name resolution",
|
||||
"Name or service not known",
|
||||
"nodename nor servname provided, or not known",
|
||||
"getaddrinfo failed",
|
||||
"No address associated with hostname",
|
||||
"Network is unreachable",
|
||||
],
|
||||
)
|
||||
def test_network_resolution_failure_explains_that_the_user_may_be_offline(
|
||||
technical_message,
|
||||
):
|
||||
error = OSError(-3, technical_message)
|
||||
|
||||
summary = AIAgent._summarize_api_error(error)
|
||||
|
||||
assert summary == (
|
||||
"Hermes can't reach the model provider. You may be offline. "
|
||||
"Check your internet connection and try again."
|
||||
)
|
||||
assert "name resolution" not in summary.lower()
|
||||
|
||||
|
||||
def test_wrapped_dns_resolution_failure_gets_the_same_friendly_message():
|
||||
try:
|
||||
try:
|
||||
raise OSError(-3, "Temporary failure in name resolution")
|
||||
except OSError as cause:
|
||||
raise RuntimeError("Connection error.") from cause
|
||||
except RuntimeError as error:
|
||||
summary = AIAgent._summarize_api_error(error)
|
||||
|
||||
assert "You may be offline" in summary
|
||||
assert "Connection error" not in summary
|
||||
|
||||
|
||||
def test_unread_streaming_response_does_not_crash_and_falls_back_to_exception_message():
|
||||
|
|
@ -62,4 +96,3 @@ def test_unread_streaming_response_does_not_crash_and_falls_back_to_exception_me
|
|||
summary = AIAgent._summarize_api_error(err)
|
||||
assert "HTTP 429" in summary
|
||||
assert "Gemini HTTP 429: quota exceeded" in summary
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue