Skip to content

fix(cli): normalize proxy environment values httpx cannot parse - #160

Open
fenghonghao wants to merge 1 commit into
PhyAgentOS:mainfrom
fenghonghao:fix/no-proxy-ipv6-cidr
Open

fenghonghao wants to merge 1 commit into
PhyAgentOS:mainfrom
fenghonghao:fix/no-proxy-ipv6-cidr

Conversation

@fenghonghao

Copy link
Copy Markdown

Problem

With a proxy configured (HTTP(S)_PROXY / ALL_PROXY), httpx 0.28.1 (the latest release) cannot construct any Client / AsyncClient when the proxy environment contains one of these values:

Value Seen in httpx error
NO_PROXY=[::1], [fd00::1]:8080 Clash Verge / mihomo bypass lists InvalidURL: Invalid port: ':1]'
NO_PROXY=::1/128, fd00::/8 (any IPv6 range) OrbStack, Tailscale-aware bypass lists InvalidURL: Invalid port: ...
NO_PROXY=中国.cn (non-ASCII host) Hand-written bypass lists InvalidURL: Invalid IDNA hostname
ALL_PROXY=socks://127.0.0.1:7897 Clash, V2RayN, WSL ValueError: Unknown scheme for proxy URL

httpx turns every NO_PROXY entry into a URL pattern: bracketed IPv6 falls through to the wildcard branch (all://*[::1]), IPv6 ranges are wrapped with their mask inside the brackets (all://[fd00::/8]), and host entries get a * prefix that IDNA rejects for non-ASCII names.

In PhyAgentOS this breaks model discovery in paos provider configure, paos provider test, and LLM calls (the openai SDK and LiteLLM also use httpx). describe_provider_error reports Endpoint configuration error; check API Base and API compatibility., which points users at API Base instead of their proxy environment, and the configure wizard refuses to save.

Upstream: encode/httpx#2659 and encode/httpx#3741 (open); pydantic/httpx2#1242 (closed).

Fix

New PhyAgentOS/utils/proxy.py with normalize_proxy_env(), called from the paos Typer callback before any subcommand runs. It rewrites values for the current process only:

Input Result Why
socks://host:port (any case) socks5://host:port httpx rejects the alias; socksio is already a dependency
[::1], [fd00::1]:8080, [fe80::1%eth0] ::1, fd00::1, fe80::1%eth0 httpx compiles the bare form into a working bypass; a port-scoped IPv6 bypass is not expressible, so the port is dropped
::1/128, [::1]/128 ::1 /128 is one address, so local IPv6 services keep bypassing the proxy
fd00::/8, fe80::/10, ::1/129 dropped httpx matches NO_PROXY entries as host patterns, never as CIDR ranges, so these never bypassed anything in httpx; they only crashed it
中国.cn, .中国.cn dropped the punycode form does not match consistently in httpx
IPv4 ranges, host names, wildcards, <local>, URL-form entries unchanged httpx accepts them

Variable names match case-insensitively, as in urllib.request.getproxies() (NO_PROXY, no_proxy, No_Proxy, Https_Proxy). NO_PROXY entries may be separated by , or ; (Windows bypass-list format) and are rejoined with , without duplicates. Values that need no change are left as they are.

Requests to dropped entries now use the proxy. System and proxy-tool settings are untouched, so other programs keep their behavior. Subprocesses started by the CLI (such as the verification service) inherit the normalized environment. Malformed values that cannot be repaired, such as socks4:// (unsupported by httpx) or a non-numeric port, still fail loudly.

This follows the workarounds other httpx-based projects use: NousResearch/hermes-agent#131802 (merged; unbrackets IPv6, rewrites CIDR to the address, rewrites socks://), MoonshotAI/kimi-cli#2000 (drops IPv6 ranges, rewrites socks://), and nexu-io/open-design#7283 (drops IPv6 ranges, unbrackets IPv6, splits on ;, keeps IPv4 ranges).

Docs

Added a Troubleshooting row to docs/en/02-user-manual.md and docs/zh/02-user-manual.md covering the symptoms, the startup normalization, and that scripts importing PhyAgentOS directly (not through paos) must adjust these variables themselves.

Tests

New tests/test_proxy_env.py (42 tests, proxy variables cleared for each test):

  • Every row of the table above, for NO_PROXY, no_proxy and No_Proxy.
  • Supported entries (example.com, .corp, *.internal, 10.0.0.0/8, [127.0.0.1], fe80::1%eth0, http://[::1]:8080, *) are unchanged.
  • socks:// in any case is rewritten in ALL_PROXY, all_proxy, HTTPS_PROXY, http_proxy and Https_Proxy; socks5://, socks5h:// and http:// URLs are unchanged.
  • With broken values set, httpx.Client() builds after normalization and http://[::1]:8000/ bypasses the proxy.
  • Invoking paos provider list through CliRunner normalizes NO_PROXY. This test fails if the callback call is removed.

ruff check passes on the changed files. Full suite: 644 passed.

Note on tests/test_skill_runtime_manifest.py::test_static_package_index_resolves_direct_skill_and_node, reported in #159 as failing on the base commit: it fails on main for this same reason whenever the developer's NO_PROXY contains an IPv6 range, and passes once that entry is removed. The test does not go through the CLI entry, so run on its own in such an environment it still fails; in the full suite it passes because earlier CLI tests normalize the process environment.

Manual check: with a real proxy environment whose NO_PROXY includes an IPv6 range, and again with No_Proxy='localhost;[::1];中国.cn' plus Https_Proxy=socks://127.0.0.1:7897, paos provider test deepseek with a dummy key returns Authentication failed (the request reached the API) instead of Endpoint configuration error.

🤖 Generated with Claude Code

With a proxy configured, httpx <= 0.28.1 fails to build any client when
the environment contains a socks:// proxy URL, or NO_PROXY entries such
as [::1], ::1/128, fd00::/8 or a non-ASCII host name. Provider
configure/test and model calls then fail with a misleading "Endpoint
configuration error".

At CLI startup, rewrite these values for the paos process only, following
the workarounds used by other httpx-based projects: socks:// becomes
socks5://, bracketed IPv6 entries lose their brackets, /128 entries
become the address, and wider IPv6 ranges and non-ASCII host entries are
dropped. Variable names match case-insensitively as in urllib, and
NO_PROXY also accepts ';' separators.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 2, 2026 13:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants