Skip to content

feat(python-sdk): move the REST API client onto pyqwest's httpx transport adapter#1601

Draft
mishushakov wants to merge 4 commits into
mainfrom
migrate-rest-to-pyqwest
Draft

feat(python-sdk): move the REST API client onto pyqwest's httpx transport adapter#1601
mishushakov wants to merge 4 commits into
mainfrom
migrate-rest-to-pyqwest

Conversation

@mishushakov

@mishushakov mishushakov commented Jul 23, 2026

Copy link
Copy Markdown
Member

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 and ApiClient/AsyncApiClient keep 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_transport now returns a pyqwest-backed httpx transport — SyncHTTPTransport/HTTPTransport (with tls_include_system_certs=True, proxy, and the pool tuning mapped from E2B_KEEPALIVE_EXPIRY/E2B_MAX_KEEPALIVE_CONNECTIONS) wrapped in a connection-only retry middleware (E2B_CONNECTION_RETRIES, matching the connect-only retries of the httpx transports this replaces), wrapped in the httpx adapter.
  • pyqwest transports are thread-safe and loop-independent (I/O runs on a Rust tokio runtime), so the caches are process-global keyed by proxy — previously one pool per thread (sync) / per event loop (async). Mirrors the transport layout of feat(python-sdk): migrate envd RPC to the official connectrpc client #1558.
  • ApiClient sheds its threading machinery: the transport_factory/async_transport_factory plumbing, the thread-local httpx.Client cache, and the per-loop WeakKeyDictionary of AsyncClients 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.Client is 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 override close()/aclose().
  • Host header: the adapter forwards the Host header httpx auto-adds, and sending an explicit host on an HTTP/2 connection makes the E2B API edge reset the stream with PROTOCOL_ERROR (reproduced with plain pyqwest against api.e2b.app). The ApiPyqwestTransport subclasses strip it; hyper derives Host/:authority from the URL. Probably worth an upstream report — pyqwest.httpx's TRANSPORT_HEADERS strips connection/transfer-encoding/etc. but not host.
  • Proxy: pyqwest takes a proxy URL string. proxy= accepts str, httpx.URL, and httpx.Proxy when it reduces to a plain proxy URL (httpx.Proxy auth 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.Proxy extras pyqwest can't express (custom headers, an ssl_context) raise InvalidArgumentException rather than being silently dropped.
  • HTTP/2: negotiated via ALPN for TLS connections (reqwest default), equivalent to the http2=True transports this replaces.

Timeout semantics note

request_timeout was 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 what request_timeout promises.

Testing

  • tests/test_api_client_transport.py rewritten 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.
  • Integration against production API (real key): 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 with RemoteProtocolError: StreamReset until the Host strip, so they genuinely exercise the new stack.)
  • Lint (ruff), typecheck (ty), unit suite (tests/ minus integration dirs): green.

Usage example

No API changes for the common path:

from e2b import Sandbox

sbx = Sandbox.create()          # control-plane calls now go through pyqwest
Sandbox.list()
sbx.kill()

The behavior change — httpx.Proxy works only when it reduces to a URL:

Sandbox.create(proxy="http://user:pass@localhost:8030")       # ok (unchanged)
Sandbox.create(proxy=httpx.Proxy("http://localhost:8030",
                                 auth=("user", "pass")))      # ok — folded into the URL
Sandbox.create(proxy=httpx.Proxy("http://localhost:8030",
                                 headers={"X-Auth": "t"}))    # raises InvalidArgumentException

🤖 Generated with Claude Code

…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-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 249a147

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@e2b/python-sdk Minor

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

@cla-bot cla-bot Bot added the cla-signed label Jul 23, 2026
@cursor

cursor Bot commented Jul 23, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Swaps all control-plane HTTP I/O to pyqwest, changes connection pooling and proxy semantics, and shares async httpx clients across loops—any of these can break production API access or concurrency.

Overview
REST API get_transport / get_api_client now use pyqwest (PyqwestTransport / AsyncPyqwestTransport) with connect-only retries, process-global pools keyed by proxy URL, and Host stripped before HTTP/2. ApiClient drops transport_factory and per-thread/per-loop httpx client caches; proxy_to_url narrows proxies and rejects unsupported httpx.Proxy options. pyqwest is added as a dependency; envd get_envd_transport stays on httpx.

E2B_MAX_CONNECTIONS no longer applies to the REST API pool (reqwest has no equivalent). The async request_timeout transport test was removed; only sync timeout tests remain. One shared httpx.AsyncClient is used across different event loops; httpx documents AsyncClient for use within a single loop, so that sharing may be unsafe. The PR description describes volume streaming and template upload pyqwest work that is not in this diff.

Reviewed by Cursor Bugbot for commit 249a147. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from e3559b4. Download artifacts from this workflow run.

JS SDK (e2b@2.35.4-migrate-rest-to-pyqwest.0):

npm install ./e2b-2.35.4-migrate-rest-to-pyqwest.0.tgz

CLI (@e2b/cli@2.15.1-migrate-rest-to-pyqwest.0):

npm install ./e2b-cli-2.15.1-migrate-rest-to-pyqwest.0.tgz

Python SDK (e2b==2.34.0+migrate.rest.to.pyqwest):

pip install ./e2b-2.34.0+migrate.rest.to.pyqwest-py3-none-any.whl

mishushakov and others added 2 commits July 24, 2026 00:15
…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>
Comment thread packages/python-sdk/e2b/volume/volume_async.py Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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.

Comment thread packages/python-sdk/e2b/volume/volume_async.py Outdated
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant