Skip to content

fix(client): sanitize NO_PROXY whitespace before httpx init (fixes #3303)#3309

Open
LeSingh1 wants to merge 2 commits into
openai:mainfrom
LeSingh1:fix/sanitize-no-proxy-newlines
Open

fix(client): sanitize NO_PROXY whitespace before httpx init (fixes #3303)#3309
LeSingh1 wants to merge 2 commits into
openai:mainfrom
LeSingh1:fix/sanitize-no-proxy-newlines

Conversation

@LeSingh1
Copy link
Copy Markdown

Fixes #3303.

Bug

OpenAI() and AsyncOpenAI() raise httpx.InvalidURL at construction time when the NO_PROXY environment variable contains whitespace (newlines, tabs, or runs of spaces). This is common when NO_PROXY is set from a Docker compose file with multi-line YAML, a .env file where the value wraps, or a shell script using line continuations.

import os
os.environ['NO_PROXY'] = 'localhost\nexample.com'

from openai import OpenAI
client = OpenAI(api_key='sk-test')
# httpx.InvalidURL: Invalid non-printable ASCII character in URL, '\n' at position 16.

Root cause is in httpx._utils.get_environment_proxies: it splits NO_PROXY on commas only, so whitespace inside an entry survives into the hostname and httpx rejects it during proxy-mounts setup, before any request is made. The reporter notes the fix would be trivial in httpx but the encode/httpx project is not currently accepting external PRs, so the practical workaround is for the SDK to normalize the value just for the duration of the internal httpx client's __init__.

Fix

Introduce a _sanitized_proxy_env() context manager in _base_client.py that, while active, replaces any whitespace inside NO_PROXY / no_proxy with commas (then collapses adjacent commas). Wrap super().__init__(**kwargs) in each of the three internal httpx wrappers (_DefaultHttpxClient, _DefaultAsyncHttpxClient, _DefaultAioHttpClient). The original env var value is restored on exit so user code observes the env var it set.

with _sanitized_proxy_env():
    super().__init__(**kwargs)

If NO_PROXY contains no whitespace, the context manager is a no-op.

Verification

Added two regression tests in tests/test_client.py:

  • TestOpenAI.test_no_proxy_with_whitespace_does_not_raise
  • TestAsyncOpenAI.test_no_proxy_with_whitespace_does_not_raise

Each sets NO_PROXY="localhost\nexample.com\t192.168.1.1", constructs the default client, asserts no exception, and asserts the original env var is restored.

Local A/B:

without fix: FAILED ... httpx.InvalidURL: Invalid non-printable ASCII character in URL, '\n' at position 16
with fix:    PASSED 6 passed (new + the existing test_proxy_environment_variables and test_default_client_creation, sync + async)

Scope

40-line change to _base_client.py (one helper + three two-line with: wraps + three import additions) plus 26 lines of test coverage. No behavior change for users who do not set NO_PROXY or who set it with valid comma-separated values.

NO_PROXY values that contain newlines or tabs (common when set from
Docker, .env files, or shell scripts that wrap lines) used to break
OpenAI() / AsyncOpenAI() construction with httpx.InvalidURL: the
hostname token includes the whitespace and httpx rejects it during
its proxy-mounts setup before any request is made.

The underlying parse bug is in httpx (get_environment_proxies splits
only on commas), but the encode/httpx project is not currently
accepting external PRs, so the practical workaround is for the SDK
to normalize the value just for the duration of the internal httpx
client's __init__.

Introduce a _sanitized_proxy_env() context manager that swaps any
whitespace inside NO_PROXY / no_proxy for commas, wraps each of the
three internal httpx wrappers (_DefaultHttpxClient,
_DefaultAsyncHttpxClient, _DefaultAioHttpClient), and restores the
original value on exit so user code observes the env var it set.

Fixes openai#3303.
@LeSingh1 LeSingh1 requested a review from a team as a code owner May 26, 2026 16:19
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6092238595

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openai/_base_client.py Outdated
Comment on lines +858 to +862
for name, original in saved.items():
if original is None:
os.environ.pop(name, None)
else:
os.environ[name] = original
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard env restoration against concurrent client initialization

The new _sanitized_proxy_env() mutates process-wide NO_PROXY/no_proxy without synchronization, so overlapping client constructions can leave the environment permanently altered. If thread A sanitizes NO_PROXY, thread B enters during that window and snapshots the already-sanitized value, then A restores the original and B later restores its stale snapshot, resulting in NO_PROXY being left sanitized instead of the caller’s original value. Because this variable is global, that leaked value can change proxy behavior for subsequent HTTP clients and subprocesses in multithreaded applications.

Useful? React with 👍 / 👎.

The contextmanager added in 6092238 mutates os.environ to clean up
NO_PROXY whitespace, which is process-wide and not thread-safe. Two
clients constructed on different threads can interleave: thread A
saves the original and cleans the env, thread B enters during that
window and snapshots the already-cleaned value as its 'original',
then both threads restore in turn, leaving NO_PROXY permanently
sanitized for the rest of the process.

Wrap the env mutation in a module-level threading.Lock so the
save/clean/restore sequence runs atomically. Add a concurrency
regression test that spins up 8 threads constructing 20 clients
each and asserts NO_PROXY equals the caller's original string at
the end.

Codex review caught this on the original PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

InvalidURL error when NO_PROXY environment variable contains newline characters

1 participant