diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index 216b36aabd..692af116b3 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 @@ -831,11 +832,27 @@ def _idempotency_key(self) -> str: return f"stainless-python-retry-{uuid.uuid4()}" +def _sanitize_proxy_env() -> None: + """Sanitize proxy-related environment variables that may contain newline characters. + + Some environments (Docker .env files, shell scripts) introduce newline characters + into NO_PROXY / no_proxy values. httpx only splits these by comma, so newlines + end up embedded in hostnames and trigger ``httpx.InvalidURL``. + """ + for key in ("NO_PROXY", "no_proxy"): + value = os.environ.get(key) + if value and "\n" in value: + os.environ[key] = ",".join( + part.strip() for part in value.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) + _sanitize_proxy_env() super().__init__(**kwargs) @@ -1423,6 +1440,7 @@ def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("timeout", DEFAULT_TIMEOUT) kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) kwargs.setdefault("follow_redirects", True) + _sanitize_proxy_env() super().__init__(**kwargs) @@ -1440,7 +1458,7 @@ def __init__(self, **kwargs: Any) -> None: kwargs.setdefault("timeout", DEFAULT_TIMEOUT) kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) kwargs.setdefault("follow_redirects", True) - + _sanitize_proxy_env() super().__init__(**kwargs) diff --git a/tests/test_client.py b/tests/test_client.py index 2d8955a58e..cd187c58e6 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1292,6 +1292,48 @@ def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> N assert len(mounts) == 1 assert mounts[0][0].pattern == "https://" + def test_no_proxy_newline_sanitized(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("NO_PROXY", "localhost\n192.168.1.1") + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("HTTPS_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) + + client = DefaultHttpxClient() + assert client is not None + assert os.environ["NO_PROXY"] == "localhost,192.168.1.1" + + def test_no_proxy_lowercase_newline_sanitized(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("no_proxy", "localhost\n192.168.1.1\n10.0.0.0/8") + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("HTTPS_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + + client = DefaultHttpxClient() + assert client is not None + assert os.environ["no_proxy"] == "localhost,192.168.1.1,10.0.0.0/8" + + def test_no_proxy_without_newlines_unchanged(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("NO_PROXY", "localhost,192.168.1.1") + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("HTTPS_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) + + client = DefaultHttpxClient() + assert client is not None + assert os.environ["NO_PROXY"] == "localhost,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 @@ -2552,6 +2594,48 @@ 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_newline_sanitized(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("NO_PROXY", "localhost\n192.168.1.1") + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("HTTPS_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) + + client = DefaultAsyncHttpxClient() + assert client is not None + assert os.environ["NO_PROXY"] == "localhost,192.168.1.1" + + async def test_no_proxy_lowercase_newline_sanitized(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("no_proxy", "localhost\n192.168.1.1\n10.0.0.0/8") + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("HTTPS_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + + client = DefaultAsyncHttpxClient() + assert client is not None + assert os.environ["no_proxy"] == "localhost,192.168.1.1,10.0.0.0/8" + + async def test_no_proxy_without_newlines_unchanged(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("NO_PROXY", "localhost,192.168.1.1") + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("HTTPS_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) + + client = DefaultAsyncHttpxClient() + assert client is not None + assert os.environ["NO_PROXY"] == "localhost,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