Skip to content

fix(ai): work around httpx NO_PROXY IPv6 CIDR crash#463

Merged
dingyufei615 merged 1 commit intoUsagi-org:masterfrom
lennondotw:fix/httpx-no-proxy-ipv6-cidr
Apr 20, 2026
Merged

fix(ai): work around httpx NO_PROXY IPv6 CIDR crash#463
dingyufei615 merged 1 commit intoUsagi-org:masterfrom
lennondotw:fix/httpx-no-proxy-ipv6-cidr

Conversation

@lennondotw
Copy link
Copy Markdown
Contributor

@lennondotw lennondotw commented Apr 13, 2026

Problem

OrbStack / Docker Desktop automatically injects proxy environment variables into containers:

NO_PROXY=localhost,127.0.0.0/8,::1/128

httpx <= 0.28.1's get_environment_proxies() wraps IPv6 CIDR entries from NO_PROXY inside brackets including the CIDR mask (e.g. all://[::1/128]), causing the URL parser to misinterpret /128 as a port and throw Invalid port: ':1'.

This crashes AsyncOpenAI client initialization, making AI analysis completely unavailable.

Root Cause

In httpx _utils.get_environment_proxies():

elif is_ipv6_hostname(hostname):           # "::1/128" -> True
    mounts[f"all://[{hostname}]"] = None   # -> "all://[::1/128]"

is_ipv6_hostname correctly identifies IPv6 via split("/")[0], but the URL construction includes the CIDR mask inside brackets. URLPattern then parses [::1/128] and treats /128] as part of the authority, ultimately failing on int(":1").

Upstream fix PR (encode/httpx#3741) has passing CI but remains unmerged. httpx has not released for over 16 months.

Fix

Call _sanitize_no_proxy_env() before creating AsyncOpenAI in AIClient._initialize_client. This strips CIDR prefix lengths from IPv6 entries in NO_PROXY / no_proxy (e.g. ::1/128 -> ::1).

  • IPv4 CIDR entries (e.g. 10.0.0.0/8) are preserved
  • No functional loss: httpx 0.28.1 doesn't support CIDR range matching anyway
  • Once httpx merges the upstream fix, this workaround becomes a harmless no-op

Tests

5 new unit tests covering: IPv6 CIDR stripping, case variants (NO_PROXY / no_proxy), IPv4 CIDR preservation, missing env vars, and both keys present simultaneously.

Verified in container: AIClient initializes successfully with NO_PROXY=localhost,127.0.0.0/8,::1/128.

httpx <= 0.28.1 wraps NO_PROXY IPv6 entries including the CIDR mask
inside brackets (e.g. `[::1/128]`), which its URL parser rejects as
`Invalid port: ':1'`. This is triggered by OrbStack/Docker Desktop
injecting `NO_PROXY=localhost,127.0.0.0/8,::1/128` into containers.

Strip the `/prefix` from IPv6 CIDR entries before initializing
AsyncOpenAI. This is safe because httpx doesn't support CIDR range
matching anyway — it only does exact-host comparison.

Upstream fix: encode/httpx#3741 (pending merge)
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a _sanitize_no_proxy_env utility to strip CIDR prefixes from IPv6 entries in the NO_PROXY environment variables, addressing a known issue in httpx. It also includes comprehensive unit tests for this logic. Feedback suggests optimizing the IPv6 detection logic to avoid unnecessary exception handling for IPv4 addresses and cleaning up unused variables.

Comment on lines +55 to +64
if "/" in part:
host, _, prefix = part.partition("/")
try:
ipaddress.IPv6Address(host)
cleaned.append(host)
changed = True
continue
except ValueError:
pass
cleaned.append(part)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Optimization: Adding a check for : before attempting to parse the host as an IPv6 address can avoid the overhead of the try/except block for IPv4 CIDRs (e.g., 127.0.0.0/8) and other non-IPv6 entries in NO_PROXY. Additionally, the prefix variable is unused and can be replaced with _ to follow Python conventions.

Suggested change
if "/" in part:
host, _, prefix = part.partition("/")
try:
ipaddress.IPv6Address(host)
cleaned.append(host)
changed = True
continue
except ValueError:
pass
cleaned.append(part)
if "/" in part and ":" in part:
host, _, _ = part.partition("/")
try:
ipaddress.IPv6Address(host)
cleaned.append(host)
changed = True
continue
except ValueError:
pass
cleaned.append(part)

@dingyufei615
Copy link
Copy Markdown
Collaborator

thanks bro

@dingyufei615 dingyufei615 merged commit 80e0af6 into Usagi-org:master Apr 20, 2026
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