fix(sync-httpx): respect proxy env vars when force_ipv4 is enabled

When `force_ipv4: true` is set in config, the sync HTTPHandler creates a
custom `HTTPTransport(local_address="0.0.0.0")` which causes httpx to
ignore `HTTP_PROXY`/`HTTPS_PROXY` environment variables. This breaks
providers that use the sync client path (e.g. ChatGPT authenticator)
when running behind a proxy.

The fix detects proxy env vars at client creation time and skips the
custom transport so httpx auto-detects proxy settings. When no proxy is
configured, the IPv4-forcing transport is used as before.

Closes #24562
See also #24983
This commit is contained in:
z 2026-05-09 00:48:20 +08:00
parent c011a7e3ba
commit 81e79f62c6
2 changed files with 71 additions and 0 deletions

View file

@ -1047,6 +1047,19 @@ class HTTPHandler:
if client is None:
transport = self._create_sync_transport()
# When proxy is configured via env vars, skip custom transport.
# httpx ignores proxy env vars when an explicit transport is
# provided, which breaks providers that use the sync client
# (e.g. ChatGPT authenticator) behind a proxy.
proxy_url = (
os.environ.get("HTTPS_PROXY")
or os.environ.get("https_proxy")
or os.environ.get("HTTP_PROXY")
or os.environ.get("http_proxy")
)
if proxy_url and transport is not None:
transport = None
# Create a client with a connection pool
self.client = httpx.Client(
transport=transport,

View file

@ -675,6 +675,64 @@ async def test_httpx_handler_uses_env_user_agent(monkeypatch):
await handler.close()
def test_sync_http_handler_skips_custom_transport_when_proxy_set(monkeypatch):
"""
When HTTPS_PROXY is set and force_ipv4 creates a custom transport,
the sync HTTPHandler must skip the custom transport so that httpx
auto-detects proxy from environment variables.
httpx ignores proxy env vars when an explicit transport is provided,
which breaks sync-path providers (e.g. ChatGPT authenticator) behind
a proxy. See GitHub issue #24562.
"""
original_force_ipv4 = litellm.force_ipv4
litellm.force_ipv4 = True
try:
# With proxy: custom transport should be dropped
monkeypatch.setenv("HTTPS_PROXY", "http://proxy:8080")
handler = HTTPHandler()
try:
transport = handler.client._transport
assert not isinstance(transport, httpx.HTTPTransport)
finally:
handler.close()
# Without proxy: custom transport should be applied (force_ipv4)
monkeypatch.delenv("HTTPS_PROXY", raising=False)
monkeypatch.delenv("HTTP_PROXY", raising=False)
handler2 = HTTPHandler()
try:
transport2 = handler2.client._transport
assert isinstance(transport2, httpx.HTTPTransport)
finally:
handler2.close()
finally:
litellm.force_ipv4 = original_force_ipv4
def test_sync_http_handler_prefers_https_proxy_over_http_proxy(monkeypatch):
"""
HTTPS_PROXY takes precedence over HTTP_PROXY, matching httpx convention.
"""
original_force_ipv4 = litellm.force_ipv4
litellm.force_ipv4 = True
try:
monkeypatch.setenv("HTTPS_PROXY", "http://https-proxy:8080")
monkeypatch.setenv("HTTP_PROXY", "http://http-proxy:8080")
handler = HTTPHandler()
try:
# Should not have custom transport (proxy takes priority)
transport = handler.client._transport
assert not isinstance(transport, httpx.HTTPTransport)
finally:
handler.close()
finally:
litellm.force_ipv4 = original_force_ipv4
def test_get_httpx_client_applies_float_timeout_without_mocking_handler():
"""
Exercise real _get_httpx_client + HTTPHandler: params={'timeout': x} must reach httpx.Client(timeout=...).