Skip to content

Commit ac60c0e

Browse files
authored
fix: respect HTTP(S)_PROXY env vars for httpx client (#2632)
1 parent 85debd3 commit ac60c0e

2 files changed

Lines changed: 69 additions & 18 deletions

File tree

cognite/client/_http_client.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,30 +53,17 @@ def get_global_async_httpx_client() -> httpx.AsyncClient:
5353
return _global_async_httpx_clients[loop]
5454
except KeyError:
5555
pass
56-
async_transport = httpx.AsyncHTTPTransport(
56+
57+
client = _global_async_httpx_clients[loop] = httpx.AsyncClient(
5758
proxy=global_config.proxy,
58-
retries=0, # 'retries': The maximum number of retries when trying to establish a connection.
5959
verify=not global_config.disable_ssl,
6060
limits=httpx.Limits(
61-
# max_connections: The maximum number of concurrent HTTP connections that
62-
# the pool should allow. Any attempt to send a request on a pool that
63-
# would exceed this amount will block until a connection is available.
64-
# max_keepalive_connections: The maximum number of idle HTTP connections
65-
# that will be maintained in the pool.
66-
# keepalive_expiry: The duration in seconds that an idle HTTP connection
67-
# may be maintained for before being expired from the pool.
6861
max_connections=global_config.max_connection_pool_size,
69-
max_keepalive_connections=None, # defaults to match max_connections
70-
keepalive_expiry=5, # copy httpx default
62+
max_keepalive_connections=None,
63+
keepalive_expiry=5,
7164
),
72-
)
73-
client = _global_async_httpx_clients[loop] = httpx.AsyncClient(
74-
transport=async_transport,
7565
follow_redirects=global_config.follow_redirects,
7666
cookies=NoCookiesPlease(),
77-
# Below should not be needed when we pass transport, but... :)
78-
proxy=global_config.proxy,
79-
verify=not global_config.disable_ssl,
8067
)
8168
return client
8269

tests/tests_unit/test_http_client.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
from __future__ import annotations
22

3+
import ssl
4+
from collections.abc import Iterator
5+
36
import httpx
47
import pytest
58

6-
from cognite.client._http_client import AsyncHTTPClientWithRetryConfig, RetryTracker
9+
from cognite.client._http_client import (
10+
AsyncHTTPClientWithRetryConfig,
11+
NoCookiesPlease,
12+
RetryTracker,
13+
_global_async_httpx_clients,
14+
get_global_async_httpx_client,
15+
)
16+
from cognite.client.config import global_config
717

818

919
@pytest.fixture
@@ -107,3 +117,57 @@ def test_is_auto_retryable(self, default_config: AsyncHTTPClientWithRetryConfig)
107117
# 409 is not in the list of status codes to retry, but we set is_auto_retryable=True, which should override it
108118
assert rt.should_retry_status_code(make_http_status_error(409), is_auto_retryable=True) is True
109119
assert rt.should_retry_status_code(make_http_status_error(409), is_auto_retryable=False) is False
120+
121+
122+
@pytest.fixture
123+
def _clean_global_httpx_clients() -> Iterator[None]:
124+
_global_async_httpx_clients.clear()
125+
yield
126+
_global_async_httpx_clients.clear()
127+
128+
129+
@pytest.mark.usefixtures("_clean_global_httpx_clients")
130+
class TestGetGlobalAsyncHttpxClient:
131+
async def test_no_proxy_when_env_unset_and_config_unset(self, monkeypatch: pytest.MonkeyPatch) -> None:
132+
# Ensure test env does not randomly have these set:
133+
monkeypatch.delenv("HTTPS_PROXY", raising=False)
134+
monkeypatch.delenv("HTTP_PROXY", raising=False)
135+
136+
client = get_global_async_httpx_client()
137+
138+
assert not client._mounts
139+
140+
async def test_env_proxy_is_respected(self, monkeypatch: pytest.MonkeyPatch) -> None:
141+
monkeypatch.setenv("HTTPS_PROXY", "http://magicenvproxy:666")
142+
143+
client = get_global_async_httpx_client()
144+
145+
assert len(client._mounts) == 1
146+
147+
# If the below asserts fail due to httpx/httpcore private API changes, just keep the assert above.
148+
(transport,) = client._mounts.values()
149+
assert transport._pool._proxy_url.host == b"magicenvproxy" # type: ignore[union-attr]
150+
assert transport._pool._proxy_url.port == 666 # type: ignore[union-attr]
151+
152+
async def test_explicit_proxy_config_still_works(self, monkeypatch: pytest.MonkeyPatch) -> None:
153+
monkeypatch.setattr(global_config, "proxy", "http://explicit:1234")
154+
155+
client = get_global_async_httpx_client()
156+
157+
assert client._mounts
158+
159+
async def test_client_settings_match_global_config(self, monkeypatch: pytest.MonkeyPatch) -> None:
160+
"""Verify pool limits, SSL, redirect, and cookie settings are forwarded correctly."""
161+
monkeypatch.setattr(global_config, "max_connection_pool_size", 42)
162+
monkeypatch.setattr(global_config, "disable_ssl", True)
163+
monkeypatch.setattr(global_config, "follow_redirects", True)
164+
client = get_global_async_httpx_client()
165+
166+
assert client.follow_redirects is True
167+
assert isinstance(client._cookies.jar, NoCookiesPlease)
168+
169+
pool = client._transport._pool # type: ignore[attr-defined]
170+
assert pool._max_connections == 42
171+
assert pool._keepalive_expiry == 5
172+
assert pool._ssl_context.verify_mode == ssl.CERT_NONE # disable_ssl should cause this
173+
assert pool._ssl_context.check_hostname is False

0 commit comments

Comments
 (0)