44# Copyright (c) 2025 CloudBlue. All Rights Reserved.
55#
66import contextvars
7+ import ipaddress
78import threading
89from functools import cache
910from json .decoder import JSONDecodeError
1011from typing import Union
12+ from urllib .request import getproxies
1113
1214import httpx
1315import requests
14- from httpx ._config import Proxy
15- from httpx ._utils import get_environment_proxies
1616from requests .adapters import HTTPAdapter
1717
1818from connect .client .constants import CONNECT_ENDPOINT_URL , CONNECT_SPECS_URL
@@ -240,6 +240,64 @@ def _get_namespace_class(self):
240240_SSL_CONTEXT = httpx .create_ssl_context ()
241241
242242
243+ def _is_ipv4_hostname (hostname ):
244+ try :
245+ ipaddress .IPv4Address (hostname .split ('/' )[0 ])
246+ except ValueError :
247+ return False
248+ return True
249+
250+
251+ def _is_ipv6_hostname (hostname ):
252+ try :
253+ ipaddress .IPv6Address (hostname .split ('/' )[0 ])
254+ except ValueError :
255+ return False
256+ return True
257+
258+
259+ def _get_environment_proxies ():
260+ """
261+ Build httpx mount patterns from the standard proxy environment variables
262+ (HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY and lowercase variants).
263+
264+ Ported from httpx's internal ``get_environment_proxies`` so we don't depend
265+ on the private ``httpx._utils`` module.
266+ """
267+ proxy_info = getproxies ()
268+ mounts = {}
269+
270+ for scheme in ('http' , 'https' , 'all' ):
271+ if proxy_info .get (scheme ):
272+ hostname = proxy_info [scheme ]
273+ # Default scheme for a scheme-less proxy env var, mirroring httpx's
274+ # get_environment_proxies; not an app-level insecure connection.
275+ mounts [f'{ scheme } ://' ] = (
276+ hostname if '://' in hostname else f'http://{ hostname } ' # NOSONAR
277+ )
278+
279+ no_proxy_hosts = [host .strip () for host in proxy_info .get ('no' , '' ).split (',' )]
280+ # NO_PROXY=* bypasses every proxy, so ignore all proxy configuration.
281+ if '*' in no_proxy_hosts :
282+ return {}
283+ for hostname in no_proxy_hosts :
284+ if hostname :
285+ mounts [_no_proxy_mount (hostname )] = None
286+
287+ return mounts
288+
289+
290+ def _no_proxy_mount (hostname ):
291+ """Map a single NO_PROXY entry to its httpx mount pattern."""
292+ if '://' in hostname :
293+ return hostname
294+ if _is_ipv6_hostname (hostname ):
295+ return f'all://[{ hostname } ]'
296+ if _is_ipv4_hostname (hostname ) or hostname .lower () == 'localhost' :
297+ return f'all://{ hostname } '
298+ return f'all://*{ hostname } '
299+
300+
243301@cache
244302def _get_async_mounts ():
245303 """
@@ -249,8 +307,8 @@ def _get_async_mounts():
249307 return {
250308 key : None
251309 if url is None
252- else httpx .AsyncHTTPTransport (verify = _SSL_CONTEXT , proxy = Proxy (url = url ))
253- for key , url in get_environment_proxies ().items ()
310+ else httpx .AsyncHTTPTransport (verify = _SSL_CONTEXT , proxy = httpx . Proxy (url = url ))
311+ for key , url in _get_environment_proxies ().items ()
254312 }
255313
256314
0 commit comments