Repository navigation
Replies: 1 comment
|
Corroborating this from an independent setup, on Windows. Same 2.13.1, same traceback, same What makes this case show up in practice: the DeepSeek Harness desktop app appends The constant there is: const LOOPBACK_NO_PROXY = ["localhost", "127.0.0.1", "::1", "[::1]"];So the harness writes both spellings for the interop reason described above, and one shell child ended up with:
Concrete consequence in a real workflow: a build helper inside a dev environment aborted at Environment where I reproduced it: Windows 11 x86_64, CPython 3.14.7, httpx2 2.13.1, and httpx 0.28.1 for comparison. I also found a downstream project hitting the same signature: MoonshotAI/kimi-cli#1959, where it crashes on startup and was worked around downstream rather than in httpx2. No new proposal from me — the normalization you suggested looks right, and I am not asking for anything beyond that. |
Uh oh!
There was an error while loading. Please reload this page.
What happens
With environment proxy discovery enabled, a
NO_PROXYentry that spells an IPv6 address the way a URL does can abort client construction before any request is made.httpx2 2.13.1, CPython 3.14.0, macOS arm64. The same value reproduces on httpx 0.28.1, which this fork is based on.
Why
In
get_environment_proxies()the two spellings of the same address take different paths:::1satisfiesis_ipv6_hostname()and becomesall://[::1], which is correct.[::1]does not, because the brackets are not part of the literal. It also is notlocalhost, so it falls through to the host-wildcard branch and becomesall://*[::1].URLPatternthen reads that as host*[with port:1].The spellings that carry brackets are affected:
[::1],[::1]:8080,[::1]/128.Why a bracketed entry shows up at all
Measured against a local server with the proxy pointed at a dead port:
NO_PROXYentryEnvHttpProxyAgent::1:+ port1[::1]For urllib,
Request('http://[::1]:8080/').hostis'[::1]:8080'and_splitport('::1')returns host':'with port1; undici'sparseNoProxyperforms the same split.requestsreadsurlsplit().hostname, which has the brackets stripped.So a host that wants IPv6 loopback bypassed across more than one of these stacks ends up writing both spellings.
::1is fine here;[::1]aborts construction for the clients in that environment that discover proxies from the environment.Background: what other implementations do with
[::1]no_proxyhas no specification, so matching rules are implementation-defined. For this particular entry: Go (x/net/http/httpproxy) strips the brackets and matches on the parsed address; curl hands the entry toinet_ptonunchanged, so it fails to parse and never matches; hyper-util (used by reqwest) strips brackets from the URL host, so[::1]ends up in its domain bucket and never matches. I did not find one that raises for this entry. I have not tested every implementation against every malformed entry.Proposed change
Recognize the bracketed form before the dispatch, so a bare bracketed address produces the same mount as its unbracketed spelling:
Scope. For a bare address this produces the same mount as the unbracketed spelling; the bracketed form with a port is described by its own regression cases rather than as an equivalence. The change adds no CIDR/network matching (
URLPattern.matchesdoes not consider a prefix length, which is what #969 covers), and it does not make malformed entries safe in general:[not:ipv6]still reaches the wildcard branch, and a non-numeric port such as[::1]:badstill fails to parse.I have this with tests ready, including client-construction and transport-selection behaviour, and it passes
scripts/checkand the full suite locally. Would you be open to a narrowly scoped PR for this normalization, leaving malformed-entry handling and CIDR matching as separate questions?All reactions