Skip to content

Commit 249a147

Browse files
mishushakovclaude
andcommitted
feat(python-sdk): reduce httpx.Proxy proxies to plain proxy URLs
The API-client half of the former combined commit: proxy= accepts str, httpx.URL, and httpx.Proxy when it reduces to a plain proxy URL (auth folded into the userinfo); inexpressible extras (custom headers, ssl_context) raise InvalidArgumentException. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fc1dd98 commit 249a147

3 files changed

Lines changed: 50 additions & 7 deletions

File tree

.changeset/python-pyqwest-rest-transport.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ Connection-establishment failures are retried with backoff
1818
(`E2B_CONNECTION_RETRIES`, default 3), matching the connect-only retries of
1919
the previous transports.
2020

21-
`proxy` for API calls must now be a URL string (e.g.
21+
`proxy` for API calls now takes a URL string (e.g.
2222
`proxy="http://user:pass@localhost:8030"`, scheme http, https, socks5, or
23-
socks5h); the richer `httpx.Proxy` objects are rejected with
24-
`InvalidArgumentException` rather than partially honored.
23+
socks5h). `httpx.URL` and `httpx.Proxy` keep working when they reduce to
24+
such a URL (`httpx.Proxy` auth is folded back into the URL userinfo);
25+
`httpx.Proxy` extras pyqwest can't express — custom headers, an
26+
`ssl_context` — raise `InvalidArgumentException` rather than being silently
27+
dropped.
2528

2629
envd traffic (sandbox commands, filesystem, PTY, file transfers) is not
2730
affected and stays on its existing stack.

packages/python-sdk/e2b/api/__init__.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,31 @@ async def on_response(response: Response) -> None:
8181
def proxy_to_url(proxy: Optional[ProxyTypes]) -> Optional[str]:
8282
"""Narrow the ``proxy`` connection option to the proxy URL string pyqwest
8383
transports take (scheme http, https, socks5, or socks5h, credentials in
84-
the URL userinfo). The richer ``httpx.Proxy`` objects the httpx transports
85-
accepted (per-proxy auth, headers, TLS context) are rejected rather than
86-
partially honored."""
84+
the URL userinfo). ``httpx.URL`` and ``httpx.Proxy`` — which the httpx
85+
transports accepted — are converted when they reduce to such a URL;
86+
``httpx.Proxy`` extras that don't (custom headers, an ssl_context) are
87+
rejected rather than silently dropped."""
8788
if proxy is None:
8889
return None
8990
if isinstance(proxy, str):
9091
return proxy
9192
if isinstance(proxy, httpx.URL):
9293
return str(proxy)
94+
if isinstance(proxy, httpx.Proxy):
95+
if proxy.headers:
96+
raise InvalidArgumentException(
97+
"E2B API calls don't support httpx.Proxy custom headers; "
98+
"pass credentials in the proxy URL instead, "
99+
'e.g. proxy="http://user:pass@localhost:8030"'
100+
)
101+
if proxy.ssl_context is not None:
102+
raise InvalidArgumentException(
103+
"E2B API calls don't support httpx.Proxy ssl_context"
104+
)
105+
url = proxy.url
106+
if proxy.auth is not None:
107+
url = url.copy_with(username=proxy.auth[0], password=proxy.auth[1])
108+
return str(url)
93109
raise InvalidArgumentException(
94110
"E2B API calls support only URL-string proxies, "
95111
'e.g. proxy="http://user:pass@localhost:8030"'

packages/python-sdk/tests/test_api_client_transport.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import json
3+
import ssl
34
import threading
45
from concurrent.futures import ThreadPoolExecutor
56
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -54,8 +55,31 @@ def test_proxy_to_url_narrows_to_url_strings():
5455
assert proxy_to_url(None) is None
5556
assert proxy_to_url("http://127.0.0.1:9999") == "http://127.0.0.1:9999"
5657
assert proxy_to_url(httpx.URL("http://127.0.0.1:9999")) == "http://127.0.0.1:9999"
58+
59+
60+
def test_proxy_to_url_reduces_httpx_proxy():
61+
# httpx.Proxy converts when it reduces to a plain proxy URL, with
62+
# credentials (which httpx.Proxy splits off the URL) folded back into
63+
# the userinfo.
64+
assert proxy_to_url(httpx.Proxy("http://127.0.0.1:9999")) == "http://127.0.0.1:9999"
65+
assert (
66+
proxy_to_url(httpx.Proxy("http://user:pass@127.0.0.1:9999"))
67+
== "http://user:pass@127.0.0.1:9999"
68+
)
69+
assert (
70+
proxy_to_url(httpx.Proxy("http://127.0.0.1:9999", auth=("user", "pass")))
71+
== "http://user:pass@127.0.0.1:9999"
72+
)
73+
74+
# Extras pyqwest can't express are rejected rather than silently dropped.
75+
with pytest.raises(InvalidArgumentException):
76+
proxy_to_url(httpx.Proxy("http://127.0.0.1:9999", headers={"X-Auth": "t"}))
5777
with pytest.raises(InvalidArgumentException):
58-
proxy_to_url(httpx.Proxy("http://127.0.0.1:9999"))
78+
proxy_to_url(
79+
httpx.Proxy(
80+
"https://127.0.0.1:9999", ssl_context=ssl.create_default_context()
81+
)
82+
)
5983

6084

6185
def test_connection_retry_policy_retries_only_connection_errors():

0 commit comments

Comments
 (0)