chore: bump e2b to 2.39.0 (JS) and 2.39.1 (Python) - #328
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR SummaryMedium Risk Overview In sync and async Adds Reviewed by Cursor Bugbot for commit 2d4b232. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
This is a version-bump PR, but it breaks the Python SDK: e2b 2.38.0 changed get_transport() to no longer accept the http2 kwarg, and both code_interpreter_sync.py and code_interpreter_async.py still pass http2=False. That will raise a TypeError on first use of _client in every sync/async sandbox call, so this needs a fix before merging (see inline comment).
Extended reasoning...
Verified directly against the actual e2b 2.38.0 wheel: both e2b/api/client_sync/__init__.py and e2b/api/client_async/__init__.py define get_transport(config) with a single positional parameter and no **kwargs. This repo's code_interpreter_sync.py:84 and code_interpreter_async.py:89 call get_transport(self.connection_config, http2=False), which will raise TypeError: get_transport() got an unexpected keyword argument 'http2' the first time _client is accessed — i.e., on essentially every run_code/context call in both sandbox flavors. This is a real, high-severity regression introduced by the dependency bump (the PR itself only touches lockfiles/manifests, but the new pinned version is incompatible with existing call sites), not a false positive from the finder agents. Deferring rather than approving.
e2b 2.38.0 moved the Python SDK's HTTP stack onto pyqwest, dropping the `http2` argument from the internal `get_transport()` helper. Passing it raised `TypeError` on every `_client` access, and simply dropping it would have left Jupyter requests on ALPN-negotiated HTTP/2, where a cancelled request only resets the stream and the server never sees the disconnect. Build the transport in `e2b_code_interpreter.transport` instead, with `http_version` pinned to HTTP/1.1 and the SDK's pool tuning and connect-only retry policy, so client disconnects still arrive as a TCP close and long-running executions stay cancellable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This reverts commit d268be9. Duplicating the SDK's transport internals here only deepened the private-API coupling that broke in the first place, so the HTTP-version option is going upstream instead — see .context/upstream-http1-transport-proposal.md. The `get_transport(config, http2=False)` call sites are the end state already and need no change once e2b restores the parameter; the Python floor in pyproject.toml then moves to that release. Until it ships, the Python half of this bump cannot pass CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es (#1671) The pyqwest migration in 2.38.0 dropped the `http2` parameter from `get_transport` and `get_envd_transport` (added deliberately in #1347, 2.32.0) and collapsed the transport cache key to the proxy alone, so `e2b-code-interpreter`'s Jupyter requests — `get_transport(self.connection_config, http2=False)` — now raise `TypeError: get_transport() got an unexpected keyword argument 'http2'`; that is already live, since `e2b = "^2.26.0"` resolves to 2.38.x, and it blocks the Python half of code-interpreter [#328](e2b-dev/code-interpreter#328). pyqwest supports the capability, it just was not threaded through: this restores the pre-2.38.0 signature (so no consumer code changes, only an `e2b` floor bump) by passing `http_version=None if http2 else HTTPVersion.HTTP1` into the pyqwest transports, and puts the HTTP version back into both cache keys — without that, whichever caller asks second is handed a transport of the wrong version. The default is unchanged: `None` leaves TLS connections to ALPN (HTTP/2 against the E2B API) and uses HTTP/1 for plaintext, exactly as today. HTTP/1.1 is not cosmetic for the consumer — with HTTP/2 multiplexing, abandoning a request only resets its stream, so the code-interpreter server never sees the `http.disconnect` it needs to interrupt the kernel, while HTTP/1.1's one connection per request closes the connection and the server observes it. ## Usage Both factories are internal (nothing is exported from `e2b/__init__.py`), so there is no public API change; consumers reaching into them get the 2.32.0 call back: ```python from e2b.api.client_sync import get_transport, get_envd_transport # Unchanged: ALPN negotiates the version (HTTP/2 against the E2B API). transport = get_transport(config) # Its own pool, pinned to HTTP/1.1, so a cancelled request closes the # connection and the server observes the disconnect. http1 = get_transport(config, http2=False) envd_http1 = get_envd_transport(config, http2=False) ``` The async mirror (`e2b.api.client_async`) is identical. ## Tests Six new cases in `packages/python-sdk/tests/test_api_client_transport.py`, sync and async: cache separation and identity across `http2` / proxy / `for_streaming`, the `http_version` value actually reaching the pyqwest transport (`[None, HTTP1, HTTP1]`), and a round trip proving the pinned transport works. The negotiated version can't be observed locally — the test echo server is plaintext, where both settings speak HTTP/1 — so it is asserted at the constructor, with the reason in a comment; it was verified by hand against `https://api.e2b.app/health` via the `pyqwest.access` logger, which shows `"HTTP/2 200 OK"` on the default and `"HTTP/1.1 200 OK"` with `http2=False` on both factories (and confirms `httpx.Response.http_version` is unreliable through the adapter — it reports HTTP/1.1 either way). 256 unit tests pass, plus `make lint`, `make typecheck` and `make format`. No JS change: its transport is an undici-dispatcher `fetch` with no HTTP-version knob, and the JS half of code-interpreter #328 is a clean bump. Closes [SDK-335](https://linear.app/e2b/issue/SDK-335/python-sdk-get-transport-lost-its-http2-parameter-in-2380-breaking-e2b) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
e2b 2.39.1 restores the `http2` parameter on `get_transport` / `get_envd_transport` that the pyqwest migration dropped in 2.38.0, so the HTTP/1.1 transport for Jupyter requests works again with no changes on our side. The Python floor is 2.39.1 rather than 2.39.0 because anything in `>=2.38.0, <2.39.1` raises TypeError on every code execution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The SDK's pyqwest-backed transport does not implement httpx's per-phase timeouts; it derives one whole-request deadline and takes the longest non-connect phase. Passing `(request_timeout, timeout, request_timeout, request_timeout)` therefore yielded a deadline of `max(timeout, request_timeout)`, silently discarding any `timeout` shorter than `request_timeout` (60s by default). That is what broke test_subsequent_execution_works_after_client_timeout: `run_code(timeout=3)` ran for ~57s instead of 3s, outliving the fixture's 60s sandbox, so the next call got a 502 "sandbox was not found" reported as a sandbox TimeoutException. Put `timeout` on every non-connect phase so the derived deadline is exactly `timeout`, and pass an all-None Timeout for `timeout=0` so a deliberately unbounded execution isn't capped by the connect timeout instead. `timeout` is now a hard deadline on the request rather than an idle read bound, which matches the documented meaning and the JS SDK's body-abort timer. Add tests/test_execute_timeout.py, which runs offline and fails on a bare tuple or a request_timeout left on the write or pool phase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bumps
e2bin both packages — JS^2.28.0→^2.39.0and Python^2.26.0→^2.39.1— with both lockfiles regenerated and a patch changeset. The Python side needed two fixes, both fallout from the SDK'spyqwestmigration in 2.38.0.1. The
http2parameter (fixed upstream). Jupyter requests pin HTTP/1.1 viaget_transport(config, http2=False), and 2.38.0 dropped that parameter, so every_clientaccess raisedTypeError. It was an intentional SDK capability — the transport cache key was literally(http2, proxy)in 2.32.0 — and the migration removed it without a release note, since the helper is internal.@e2b/python-sdk@2.39.1restores it on bothget_transportandget_envd_transport, so no code change is needed here; the floor is^2.39.1because anything in>=2.38.0, <2.39.1breaks every execution. Pinning HTTP/1.1 matters because HTTP/2 multiplexing means a cancelled request resets only the stream, so the server never receives thehttp.disconnectthattemplate/server/messaging.pyrelies on to interrupt the kernel.2.
run_code'stimeoutwas inert (fixed here). The new transport does not implement httpx's per-phase timeouts — it derives one whole-request deadline and takes the longest non-connect phase. Passingtimeoutas the read timeout alongsiderequest_timeouton write and pool made the effective deadlinemax(timeout, request_timeout), silently discarding anytimeoutbelow 60s. That is what failed CI:run_code(timeout=3)ran ~57s, outliving the fixture's 60s sandbox, and the next call returned a 502"The sandbox was not found"surfaced as a sandboxTimeoutException.timeoutnow bounds the request on its own, andtimeout=0disables the deadline rather than inheriting the connect timeout.Note that
timeoutis consequently a hard deadline on the request rather than an idle read bound, so an execution streaming output continuously for longer thantimeoutis now cut off. That matches the documented meaning and the JS SDK's body-abort timer (js/src/sandbox.ts:256), and the bumped SDK already bounded such executions byrequest_timeoutregardless oftimeout; a true per-request idle bound isn't expressible on this stack, since pyqwest only offers one per transport.New
python/tests/test_execute_timeout.pycovers the regression offline (8 cases) and fails on a bare tuple or arequest_timeoutleft on the write or pool phase. Full CI is green, including both interrupt tests — which now complete in ~6-8s instead of timing out — plustsc --noEmiton the JS package, which reaches Jupyter through globalfetchand needed no change.🤖 Generated with Claude Code