From 9ac1cd164062f45f400e826554482c3ae111fcd4 Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Sun, 24 May 2026 23:08:20 +0800 Subject: [PATCH 1/2] fix: sanitize NO_PROXY newlines --- src/openai/_base_client.py | 22 ++++++++++++++++++++++ tests/test_client.py | 16 ++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index 17863bc067..f4953d7ff1 100644 --- a/src/openai/_base_client.py +++ b/src/openai/_base_client.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import sys import json import time @@ -809,11 +810,25 @@ def _idempotency_key(self) -> str: return f"stainless-python-retry-{uuid.uuid4()}" +def _sanitize_no_proxy(value: str) -> str: + if "\n" not in value and "\r" not in value: + return value + + return ",".join(part.strip() for part in value.replace("\r", ",").replace("\n", ",").split(",") if part.strip()) + + class _DefaultHttpxClient(httpx.Client): def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("timeout", DEFAULT_TIMEOUT) kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) kwargs.setdefault("follow_redirects", True) + + if kwargs.get("trust_env", True): + for key in ("NO_PROXY", "no_proxy"): + value = os.environ.get(key) + if value is not None: + os.environ[key] = _sanitize_no_proxy(value) + super().__init__(**kwargs) @@ -1388,6 +1403,13 @@ def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("timeout", DEFAULT_TIMEOUT) kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) kwargs.setdefault("follow_redirects", True) + + if kwargs.get("trust_env", True): + for key in ("NO_PROXY", "no_proxy"): + value = os.environ.get(key) + if value is not None: + os.environ[key] = _sanitize_no_proxy(value) + super().__init__(**kwargs) diff --git a/tests/test_client.py b/tests/test_client.py index 396f6dea99..c35e31fe4e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1043,6 +1043,14 @@ def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> N assert len(mounts) == 1 assert mounts[0][0].pattern == "https://" + def test_no_proxy_environment_variable_with_newlines(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("NO_PROXY", "localhost\n192.168.1.1") + + client = DefaultHttpxClient() + + patterns = {mount.pattern for mount in client._mounts} + assert patterns == {"all://localhost", "all://192.168.1.1"} + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") def test_default_client_creation(self) -> None: # Ensure that the client can be initialized without any exceptions @@ -2086,6 +2094,14 @@ async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch assert len(mounts) == 1 assert mounts[0][0].pattern == "https://" + async def test_no_proxy_environment_variable_with_newlines(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("NO_PROXY", "localhost\n192.168.1.1") + + client = DefaultAsyncHttpxClient() + + patterns = {mount.pattern for mount in client._mounts} + assert patterns == {"all://localhost", "all://192.168.1.1"} + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") async def test_default_client_creation(self) -> None: # Ensure that the client can be initialized without any exceptions From a5d226fc8d8f844116f1cf42338633159c63c8a9 Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Mon, 25 May 2026 11:08:17 +0800 Subject: [PATCH 2/2] fix: avoid persisting no_proxy sanitization --- src/openai/_base_client.py | 65 ++++++++++++++++++++++++++++++-------- tests/test_client.py | 2 ++ 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index f4953d7ff1..4d8d94f661 100644 --- a/src/openai/_base_client.py +++ b/src/openai/_base_client.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import sys import json import time @@ -817,18 +816,54 @@ def _sanitize_no_proxy(value: str) -> str: return ",".join(part.strip() for part in value.replace("\r", ",").replace("\n", ",").split(",") if part.strip()) +def _get_sanitized_environment_proxies() -> dict[str, str | None]: + from urllib.request import getproxies + + from httpx._utils import is_ipv4_hostname, is_ipv6_hostname # pyright: ignore[reportPrivateImportUsage] + + proxy_info = getproxies() + mounts: dict[str, str | None] = {} + + for scheme in ("http", "https", "all"): + if proxy_info.get(scheme): + hostname = proxy_info[scheme] + mounts[f"{scheme}://"] = hostname if "://" in hostname else f"http://{hostname}" + + no_proxy_hosts = [host.strip() for host in _sanitize_no_proxy(proxy_info.get("no", "")).split(",")] + for hostname in no_proxy_hosts: + if hostname == "*": + return {} + elif hostname: + if "://" in hostname: + mounts[hostname] = None + elif is_ipv4_hostname(hostname): + mounts[f"all://{hostname}"] = None + elif is_ipv6_hostname(hostname): + mounts[f"all://[{hostname}]"] = None + elif hostname.lower() == "localhost": + mounts[f"all://{hostname}"] = None + else: + mounts[f"all://*{hostname}"] = None + + return mounts + + class _DefaultHttpxClient(httpx.Client): + @override + def _get_proxy_map(self, proxy: Any | None, allow_env_proxies: bool) -> dict[str, httpx.Proxy | None]: + if proxy is None and allow_env_proxies: + return { + key: None if url is None else httpx.Proxy(url=url) + for key, url in _get_sanitized_environment_proxies().items() + } + + return super()._get_proxy_map(proxy, allow_env_proxies) + def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("timeout", DEFAULT_TIMEOUT) kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) kwargs.setdefault("follow_redirects", True) - if kwargs.get("trust_env", True): - for key in ("NO_PROXY", "no_proxy"): - value = os.environ.get(key) - if value is not None: - os.environ[key] = _sanitize_no_proxy(value) - super().__init__(**kwargs) @@ -1399,17 +1434,21 @@ def get_api_list( class _DefaultAsyncHttpxClient(httpx.AsyncClient): + @override + def _get_proxy_map(self, proxy: Any | None, allow_env_proxies: bool) -> dict[str, httpx.Proxy | None]: + if proxy is None and allow_env_proxies: + return { + key: None if url is None else httpx.Proxy(url=url) + for key, url in _get_sanitized_environment_proxies().items() + } + + return super()._get_proxy_map(proxy, allow_env_proxies) + def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("timeout", DEFAULT_TIMEOUT) kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) kwargs.setdefault("follow_redirects", True) - if kwargs.get("trust_env", True): - for key in ("NO_PROXY", "no_proxy"): - value = os.environ.get(key) - if value is not None: - os.environ[key] = _sanitize_no_proxy(value) - super().__init__(**kwargs) diff --git a/tests/test_client.py b/tests/test_client.py index c35e31fe4e..df4a3a1e05 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1050,6 +1050,7 @@ def test_no_proxy_environment_variable_with_newlines(self, monkeypatch: pytest.M patterns = {mount.pattern for mount in client._mounts} assert patterns == {"all://localhost", "all://192.168.1.1"} + assert os.environ["NO_PROXY"] == "localhost\n192.168.1.1" @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") def test_default_client_creation(self) -> None: @@ -2101,6 +2102,7 @@ async def test_no_proxy_environment_variable_with_newlines(self, monkeypatch: py patterns = {mount.pattern for mount in client._mounts} assert patterns == {"all://localhost", "all://192.168.1.1"} + assert os.environ["NO_PROXY"] == "localhost\n192.168.1.1" @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") async def test_default_client_creation(self) -> None: