Skip to content

chore: bump e2b to 2.39.0 (JS) and 2.39.1 (Python) - #328

Merged
mishushakov merged 5 commits into
mainfrom
bump-e2b-version-changeset
Aug 13, 2026
Merged

chore: bump e2b to 2.39.0 (JS) and 2.39.1 (Python)#328
mishushakov merged 5 commits into
mainfrom
bump-e2b-version-changeset

Conversation

@mishushakov

@mishushakov mishushakov commented Aug 11, 2026

Copy link
Copy Markdown
Member

Bumps e2b in both packages — JS ^2.28.0^2.39.0 and 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's pyqwest migration in 2.38.0.

1. The http2 parameter (fixed upstream). Jupyter requests pin HTTP/1.1 via get_transport(config, http2=False), and 2.38.0 dropped that parameter, so every _client access raised TypeError. 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.1 restores it on both get_transport and get_envd_transport, so no code change is needed here; the floor is ^2.39.1 because anything in >=2.38.0, <2.39.1 breaks every execution. Pinning HTTP/1.1 matters because HTTP/2 multiplexing means a cancelled request resets only the stream, so the server never receives the http.disconnect that template/server/messaging.py relies on to interrupt the kernel.

2. run_code's timeout was 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. Passing timeout as the read timeout alongside request_timeout on write and pool made the effective deadline max(timeout, request_timeout), silently discarding any timeout below 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 sandbox TimeoutException. timeout now bounds the request on its own, and timeout=0 disables the deadline rather than inheriting the connect timeout.

Note that timeout is consequently a hard deadline on the request rather than an idle read bound, so an execution streaming output continuously for longer than timeout is 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 by request_timeout regardless of timeout; 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.py covers the regression offline (8 cases) and fails on a bare tuple or a request_timeout left on the write or pool phase. Full CI is green, including both interrupt tests — which now complete in ~6-8s instead of timing out — plus tsc --noEmit on the JS package, which reaches Jupyter through global fetch and needed no change.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cla-bot cla-bot Bot added the cla-signed label Aug 11, 2026
@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Dependency upgrades plus a change to how code-execution deadlines are enforced on the Jupyter stream; behavior is corrected but affects every Python run_code call.

Overview
Bumps the e2b dependency to 2.39.0 in the JS package and ^2.39.1 in Python, with lockfile updates and a patch changeset. Python 2.39.1 is the minimum because 2.38.x breaks Jupyter calls that pass http2=False to pin HTTP/1.1.

In sync and async run_code, the streaming /execute request now passes httpx.Timeout so timeout sets read/write/pool and request_timeout only connect, instead of a per-phase tuple where the pyqwest stack used max(timeout, request_timeout) and ignored short execution timeouts. timeout=0 uses httpx.Timeout(None) so no execution deadline is applied.

Adds test_execute_timeout.py to assert those timeout values reach the HTTP client for sync and async run_code.

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

@claude claude 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.

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.

Comment thread python/pyproject.toml Outdated
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>
@mishushakov mishushakov changed the title chore: bump e2b to 2.38.3 (JS) and 2.38.0 (Python) chore: bump e2b to 2.38.3 (JS) / 2.38.0 (Python), fix HTTP/1.1 transport Aug 12, 2026
@mishushakov
mishushakov enabled auto-merge (squash) August 12, 2026 16:45
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>
@mishushakov mishushakov changed the title chore: bump e2b to 2.38.3 (JS) / 2.38.0 (Python), fix HTTP/1.1 transport chore: bump e2b to 2.38.3 (JS) and 2.38.0 (Python) Aug 13, 2026
@mishushakov
mishushakov disabled auto-merge August 13, 2026 15:37
mishushakov added a commit to e2b-dev/E2B that referenced this pull request Aug 13, 2026
…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>
@mishushakov mishushakov changed the title chore: bump e2b to 2.38.3 (JS) and 2.38.0 (Python) chore: bump e2b to 2.39.0 (JS) and 2.39.1 (Python) Aug 13, 2026
@mishushakov
mishushakov enabled auto-merge (squash) August 13, 2026 18:18
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>
@mishushakov
mishushakov disabled auto-merge August 13, 2026 18:42
@mishushakov
mishushakov merged commit 8d0a81b into main Aug 13, 2026
16 checks passed
@mishushakov
mishushakov deleted the bump-e2b-version-changeset branch August 13, 2026 22:48
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.

2 participants