-
Notifications
You must be signed in to change notification settings - Fork 5k
fix: sanitize newlines in NO_PROXY env var before httpx client init #3519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cb66641
1c050f6
479179e
217dc74
fb55f96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import sys | ||
| import os | ||
| import json | ||
| import time | ||
| import uuid | ||
|
|
@@ -831,11 +832,31 @@ def _idempotency_key(self) -> str: | |
| return f"stainless-python-retry-{uuid.uuid4()}" | ||
|
|
||
|
|
||
| def _sanitize_no_proxy() -> None: | ||
| """Sanitize NO_PROXY/no_proxy env vars that contain newline characters. | ||
|
|
||
| httpx's ``get_environment_proxies()`` only splits by comma, not by newline. | ||
| When NO_PROXY contains newlines (common in Docker/.env files), the newline | ||
| becomes part of the hostname and httpx raises ``InvalidURL`` (issue #3303). | ||
| """ | ||
| for key in ("NO_PROXY", "no_proxy"): | ||
| val = os.environ.get(key) | ||
| if val and "\n" in val: | ||
| os.environ[key] = ",".join( | ||
| part.strip() for part in val.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 NO_PROXY environment variable: httpx's get_environment_proxies() | ||
| # only splits by comma, not by newline. When NO_PROXY contains newlines | ||
| # (common in Docker/.env files), the newline becomes part of the hostname | ||
| # and httpx raises InvalidURL. See issue #3303. | ||
| _sanitize_no_proxy() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This call only runs in Useful? React with 👍 / 👎. |
||
| super().__init__(**kwargs) | ||
|
|
||
|
|
||
|
|
@@ -1423,6 +1444,8 @@ def __init__(self, **kwargs: Any) -> None: | |
| kwargs.setdefault("timeout", DEFAULT_TIMEOUT) | ||
| kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) | ||
| kwargs.setdefault("follow_redirects", True) | ||
| # Sanitize NO_PROXY for async clients too — see _sanitize_no_proxy. | ||
| _sanitize_no_proxy() | ||
| super().__init__(**kwargs) | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -357,11 +357,34 @@ def accumulate_event(self, event: RawResponseStreamEvent) -> ParsedResponseSnaps | |
| if output.type == "function_call": | ||
| output.arguments += event.delta | ||
| elif event.type == "response.completed": | ||
| self._completed_response = parse_response( | ||
| text_format=self._text_format, | ||
| response=event.response, | ||
| input_tools=self._input_tools, | ||
| ) | ||
| # The chatgpt.com Codex backend sometimes sends `response.output: null` | ||
| # in the consolidated `response.completed` event even when valid | ||
| # `output_item.done` events were streamed earlier (see issue #3325). | ||
| # `parse_response()` guards against `None` with `response.output or []`, | ||
| # but that would discard the already-accumulated `snapshot.output` and | ||
| # emit an empty final response. When the completed event has no | ||
| # output but the snapshot has accumulated items, inject the streamed | ||
| # items into a shallow copy of the response so `parse_response()` can | ||
| # still run its text_format / parsed_arguments logic on them. | ||
| if event.response.output is None and snapshot.output: | ||
| response_with_output = construct_type_unchecked( | ||
| type_=type(event.response), | ||
| value={ | ||
| **event.response.to_dict(), | ||
| "output": [item.to_dict() for item in snapshot.output], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| }, | ||
| ) | ||
| self._completed_response = parse_response( | ||
| text_format=self._text_format, | ||
| response=response_with_output, | ||
| input_tools=self._input_tools, | ||
| ) | ||
| else: | ||
| self._completed_response = parse_response( | ||
| text_format=self._text_format, | ||
| response=event.response, | ||
| input_tools=self._input_tools, | ||
| ) | ||
|
|
||
| return snapshot | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the default sync client,
_DefaultHttpxClient.__init__now always calls_sanitize_no_proxy(), and this line referencesoseven though_base_client.pydoes not import it. AnyOpenAI()/DefaultHttpxClient()construction therefore raisesNameErrorbefore httpx initialization, even whenNO_PROXYis unset.Useful? React with 👍 / 👎.