feat(python-sdk): move the REST API client onto pyqwest's httpx transport adapter#1601
feat(python-sdk): move the REST API client onto pyqwest's httpx transport adapter#1601mishushakov wants to merge 4 commits into
Conversation
…port adapter Swap the httpx-native HTTPTransport/AsyncHTTPTransport under the generated REST API client for pyqwest (Rust reqwest/hyper) via pyqwest.httpx. The httpx client surface is unchanged; the transports are now thread-safe and loop-independent, so the pool is cached process-wide per proxy. Strip the Host header httpx adds — forwarding it on HTTP/2 makes the API edge reset the stream with PROTOCOL_ERROR. envd traffic and the volume content client stay on httpx (the streaming download path needs httpx's per-read idle timeout, which the adapter's whole-request deadline can't express). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 249a147 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
PR SummaryHigh Risk Overview
Reviewed by Cursor Bugbot for commit 249a147. Bugbot is set up for automated code reviews on this repo. Configure here. |
Package ArtifactsBuilt from e3559b4. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-2.35.4-migrate-rest-to-pyqwest.0.tgzCLI ( npm install ./e2b-cli-2.15.1-migrate-rest-to-pyqwest.0.tgzPython SDK ( pip install ./e2b-2.34.0+migrate.rest.to.pyqwest-py3-none-any.whl |
…iClient With the pyqwest transports thread-safe and loop-independent, the transport_factory/async_transport_factory plumbing, the thread-local httpx.Client cache, and the per-loop WeakKeyDictionary of AsyncClients are dead weight: a single lazily-created httpx client (the generated base behavior, same shape the volume client already uses) serves all threads and event loops over the shared per-proxy pool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Transport identity across threads/loops is implied by the shared-client tests, sequential-loop reuse by concurrent-loop identity, and the async request-timeout wiring by the sync test through the now-shared __init__. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 676f863. Configure here.
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>
676f863 to
249a147
Compare

What
An experiment: migrate the Python SDK's REST API client traffic (sandbox lifecycle, listing, templates, volumes control plane) from httpx-native transports to pyqwest (Rust reqwest/hyper), using its httpx-compatible transport adapter (
pyqwest.httpx.PyqwestTransport/AsyncPyqwestTransport). The generated openapi client andApiClient/AsyncApiClientkeep their httpx surface — logging event hooks, per-request timeouts, headers, and redirects behave as before — only the transport underneath is swapped.This is the base of a stack:
envd RPC is explicitly not touched here; that migration is #1558. envd file-transfer transports (
get_envd_transport) stay on httpx.How
e2b/api/client_sync/__init__.py/client_async/__init__.py:get_transportnow returns a pyqwest-backed httpx transport —SyncHTTPTransport/HTTPTransport(withtls_include_system_certs=True, proxy, and the pool tuning mapped fromE2B_KEEPALIVE_EXPIRY/E2B_MAX_KEEPALIVE_CONNECTIONS) wrapped in a connection-only retry middleware (E2B_CONNECTION_RETRIES, matching the connect-onlyretriesof the httpx transports this replaces), wrapped in the httpx adapter.ApiClientsheds its threading machinery: thetransport_factory/async_transport_factoryplumbing, the thread-localhttpx.Clientcache, and the per-loopWeakKeyDictionaryofAsyncClients are gone. A single lazily-created httpx client (the generated base behavior, the same shape the volume client already uses) serves all threads and event loops;httpx.Clientis documented thread-safe and nothing below it is loop-bound. Closing that client can't tear down the shared pool — the adapter transports don't overrideclose()/aclose().Hostheader httpx auto-adds, and sending an explicithoston an HTTP/2 connection makes the E2B API edge reset the stream withPROTOCOL_ERROR(reproduced with plain pyqwest againstapi.e2b.app). TheApiPyqwestTransportsubclasses strip it; hyper derivesHost/:authorityfrom the URL. Probably worth an upstream report —pyqwest.httpx'sTRANSPORT_HEADERSstripsconnection/transfer-encoding/etc. but nothost.proxy=acceptsstr,httpx.URL, andhttpx.Proxywhen it reduces to a plain proxy URL (httpx.Proxyauth is folded back into the URL userinfo) — the same narrowing as feat(python-sdk): migrate envd RPC to the official connectrpc client #1558's envd RPC proxies;httpx.Proxyextras pyqwest can't express (custom headers, anssl_context) raiseInvalidArgumentExceptionrather than being silently dropped.http2=Truetransports this replaces.Timeout semantics note
request_timeoutwas previously httpx's per-phase timeout (connect/read/write each bounded separately, so a slow multi-phase request could exceed it in total). Through the adapter it becomes an overall deadline per API call (async: headers + body; sync: up to response headers). For the SDK's REST calls — all unary with small JSON bodies — this is a tightening, arguably closer to whatrequest_timeoutpromises.Testing
tests/test_api_client_transport.pyrewritten for the new semantics: global per-proxy transport caching, a single httpx client shared across threads/loops (including 32-way concurrent request tests against a local server), the Host-header strip (fake inner transport), the connection-only retry policy,proxy_to_url, and sync+async round-trips through a real local HTTP server exercising pyqwest end to end.tests/sync/api_sync,tests/async/api_async,test_create,test_kill,test_timeout,test_connect— all green, re-run after the client simplification. (These initially failed withRemoteProtocolError: StreamResetuntil the Host strip, so they genuinely exercise the new stack.)ruff), typecheck (ty), unit suite (tests/minus integration dirs): green.Usage example
No API changes for the common path:
The behavior change —
httpx.Proxyworks only when it reduces to a URL:🤖 Generated with Claude Code