diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 479776428b..fd2f2d8122 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -899,6 +899,51 @@ def _current_custom_base_url() -> str: return custom_base or "" +def _validate_proxy_env_urls() -> None: + """Fail fast with a clear error when proxy env vars have malformed URLs. + + Common cause: shell config (e.g. .zshrc) with a typo like + ``export HTTP_PROXY=http://127.0.0.1:6153export NEXT_VAR=...`` + which concatenates 'export' into the port number. Without this + check the OpenAI/httpx client raises a cryptic ``Invalid port`` + error that doesn't name the offending env var. + """ + from urllib.parse import urlparse + + for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", + "https_proxy", "http_proxy", "all_proxy"): + value = str(os.environ.get(key) or "").strip() + if not value: + continue + try: + parsed = urlparse(value) + if parsed.scheme: + _ = parsed.port # raises ValueError for e.g. '6153export' + except ValueError as exc: + raise RuntimeError( + f"Malformed proxy environment variable {key}={value!r}. " + "Fix or unset your proxy settings and try again." + ) from exc + + +def _validate_base_url(base_url: str) -> None: + """Reject obviously broken custom endpoint URLs before they reach httpx.""" + from urllib.parse import urlparse + + candidate = str(base_url or "").strip() + if not candidate or candidate.startswith("acp://"): + return + try: + parsed = urlparse(candidate) + if parsed.scheme in {"http", "https"}: + _ = parsed.port # raises ValueError for malformed ports + except ValueError as exc: + raise RuntimeError( + f"Malformed custom endpoint URL: {candidate!r}. " + "Run `hermes setup` or `hermes model` and enter a valid http(s) base URL." + ) from exc + + def _try_custom_endpoint() -> Tuple[Optional[OpenAI], Optional[str]]: runtime = _resolve_custom_runtime() if len(runtime) == 2: @@ -1299,6 +1344,7 @@ def resolve_provider_client( Returns: (client, resolved_model) or (None, None) if auth is unavailable. """ + _validate_proxy_env_urls() # Normalise aliases provider = _normalize_aux_provider(provider) diff --git a/run_agent.py b/run_agent.py index 956a1e9638..3a017f739a 100644 --- a/run_agent.py +++ b/run_agent.py @@ -4206,6 +4206,9 @@ class AIAgent: return False def _create_openai_client(self, client_kwargs: dict, *, reason: str, shared: bool) -> Any: + from agent.auxiliary_client import _validate_base_url, _validate_proxy_env_urls + _validate_proxy_env_urls() + _validate_base_url(client_kwargs.get("base_url")) if self.provider == "copilot-acp" or str(client_kwargs.get("base_url", "")).startswith("acp://copilot"): from agent.copilot_acp_client import CopilotACPClient diff --git a/scripts/release.py b/scripts/release.py index b40fd7c239..4e5b193229 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -62,6 +62,7 @@ AUTHOR_MAP = { "258577966+voidborne-d@users.noreply.github.com": "voidborne-d", "70424851+insecurejezza@users.noreply.github.com": "insecurejezza", "259807879+Bartok9@users.noreply.github.com": "Bartok9", + "241404605+MestreY0d4-Uninter@users.noreply.github.com": "MestreY0d4-Uninter", "268667990+Roy-oss1@users.noreply.github.com": "Roy-oss1", "241404605+MestreY0d4-Uninter@users.noreply.github.com": "MestreY0d4-Uninter", # contributors (manual mapping from git names) diff --git a/tests/agent/test_proxy_and_url_validation.py b/tests/agent/test_proxy_and_url_validation.py new file mode 100644 index 0000000000..4fd6138a4d --- /dev/null +++ b/tests/agent/test_proxy_and_url_validation.py @@ -0,0 +1,60 @@ +"""Tests for malformed proxy env var and base URL validation. + +Salvaged from PR #6403 by MestreY0d4-Uninter — validates that the agent +surfaces clear errors instead of cryptic httpx ``Invalid port`` exceptions +when proxy env vars or custom endpoint URLs are malformed. +""" +from __future__ import annotations + +import pytest + +from agent.auxiliary_client import _validate_base_url, _validate_proxy_env_urls + + +# -- proxy env validation ------------------------------------------------ + + +def test_proxy_env_accepts_normal_values(monkeypatch): + monkeypatch.setenv("HTTP_PROXY", "http://127.0.0.1:6153") + monkeypatch.setenv("HTTPS_PROXY", "https://proxy.example.com:8443") + monkeypatch.setenv("ALL_PROXY", "socks5://127.0.0.1:1080") + _validate_proxy_env_urls() # should not raise + + +def test_proxy_env_accepts_empty(monkeypatch): + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("HTTPS_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + _validate_proxy_env_urls() # should not raise + + +@pytest.mark.parametrize("key", [ + "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", + "http_proxy", "https_proxy", "all_proxy", +]) +def test_proxy_env_rejects_malformed_port(monkeypatch, key): + monkeypatch.setenv(key, "http://127.0.0.1:6153export") + with pytest.raises(RuntimeError, match=rf"Malformed proxy environment variable {key}=.*6153export"): + _validate_proxy_env_urls() + + +# -- base URL validation ------------------------------------------------- + + +@pytest.mark.parametrize("url", [ + "https://api.example.com/v1", + "http://127.0.0.1:6153/v1", + "acp://copilot", + "", + None, +]) +def test_base_url_accepts_valid(url): + _validate_base_url(url) # should not raise + + +def test_base_url_rejects_malformed_port(): + with pytest.raises(RuntimeError, match="Malformed custom endpoint URL"): + _validate_base_url("http://127.0.0.1:6153export")