Skip to content

fix: move E2b-Sandbox-Id/Port headers from control plane connect to data plane ConnectionConfig#1272

Open
WoodenStone wants to merge 3 commits intoe2b-dev:mainfrom
WoodenStone:fix/connect-sandbox-headers
Open

fix: move E2b-Sandbox-Id/Port headers from control plane connect to data plane ConnectionConfig#1272
WoodenStone wants to merge 3 commits intoe2b-dev:mainfrom
WoodenStone:fix/connect-sandbox-headers

Conversation

@WoodenStone
Copy link
Copy Markdown

Problem

The Python SDK's Sandbox.connect() currently injects E2b-Sandbox-Id and E2b-Sandbox-Port headers into the control plane request (POST /sandboxes/{sandboxID}/connect).

This causes issues for any proxy/gateway that uses these headers to distinguish between control plane and data plane traffic. Since these headers are the canonical signal for "this request targets a specific sandbox instance (data plane)", their presence on a control plane API call causes the proxy to misroute the request to the sandbox's envd endpoint instead of the API server — resulting in authentication failures (401).

The JS/TS SDK does not have this problem: connectSandbox() sends a clean control plane request without these headers, and only injects them on data plane requests via the sandbox instance's connection config.

Root Cause

Introduced in #1013 (Support overriding envd API URL), the _cls_connect method was updated to pass E2b-Sandbox-Id / E2b-Sandbox-Port directly to get_api_client() as extra headers. This means they end up on the control plane HTTP client, polluting the POST /sandboxes/{id}/connect request with data plane routing signals.

Fix

Move E2b-Sandbox-Id and E2b-Sandbox-Port from the control plane API client (get_api_client() in sandbox_api.py) to extra_sandbox_headers in the ConnectionConfig (set in main.py's _cls_connect_sandbox).

This ensures:

  • Control plane requests (via config.headersApiClient) do not carry these headers
  • Data plane requests (via config.sandbox_headers → filesystem / commands / pty / health) do carry them, as intended

The fix applies to both sync and async SDK paths (4 files).

Why this matters

These two headers serve as a data plane routing signal in the E2B protocol. Any transparent proxy, API gateway, or load balancer that multiplexes control plane and data plane traffic on a single endpoint will use their presence/absence to decide where to forward the request.

When a control plane API like connect carries these headers, it creates an ambiguous signal — the path says "control plane" but the headers say "data plane". This is not specific to any single proxy implementation; it affects any gateway that follows the protocol semantics.

Verification

  • Confirmed that after this fix, connect requests no longer carry E2b-Sandbox-Id / E2b-Sandbox-Port
  • Confirmed that subsequent data plane requests (file ops, commands, health checks) still carry these headers via sandbox_headers
  • Behavior now matches the JS/TS SDK

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 14, 2026

🦋 Changeset detected

Latest commit: 65c45bc

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 Patch

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

@mishushakov
Copy link
Copy Markdown
Member

@claude review
@codex review
@cursor review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 865 to 873
if envd_access_token is not None and not isinstance(envd_access_token, Unset):
sandbox_headers["X-Access-Token"] = envd_access_token

sandbox_headers["E2b-Sandbox-Id"] = sandbox.sandbox_id
sandbox_headers["E2b-Sandbox-Port"] = str(ConnectionConfig.envd_port)

connection_config = ConnectionConfig(
extra_sandbox_headers=sandbox_headers,
**opts,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟣 In _cls_connect_sandbox (both async sandbox_async/main.py:865-873 and sync sandbox_sync/main.py:860-868), envd_access_token and traffic_access_token are passed directly from the raw Sandbox API model to cls() without sanitizing the UNSET sentinel, unlike the _create_sandbox path which applies isinstance(token, str) else None. When a sandbox is connected with secure=False, UNSET gets stored as self.__envd_access_token, causing download_url()/upload_url() to raise ValueError because use_signature = self._envd_access_token is not None evaluates to True for the UNSET sentinel. Apply the same sanitization pattern from _create_sandbox to fix this: envd_access_token if isinstance(envd_access_token, str) else None.

Extended reasoning...

The bug: In _cls_connect_sandbox, the envd_access_token (typed Union[Unset, str]) and traffic_access_token (typed Union[None, Unset, str]) are taken directly from the raw Sandbox API response model and forwarded unsanitized to cls(...) as keyword arguments. The _create_sandbox path correctly sanitizes these via isinstance(res.parsed.envd_access_token, str) else None before constructing the response object, but _cls_connect_sandbox skips this step.

Code path that triggers it: When Sandbox.connect() or AsyncSandbox.connect() is called for a sandbox created with secure=False, the API response omits the envdAccessToken field. The generated Sandbox model sets sandbox.envd_access_token = UNSET by default (Union[Unset, str] = UNSET per the model definition). This sentinel is then passed to SandboxBase.init which stores it as self.__envd_access_token = UNSET.

Why existing code does not prevent it: The code in _cls_connect_sandbox correctly guards the header assignment — if envd_access_token is not None and not isinstance(envd_access_token, Unset) — but the same raw value is still forwarded unsanitized to cls() on the very next line.

Impact: Once UNSET is stored in SandboxBase, calls to download_url() or upload_url() fail. The check use_signature = self._envd_access_token is not None evaluates to True because UNSET is not None. This causes get_signature() to be invoked with UNSET as the token. Inside get_signature(), the guard "if not envd_access_token:" evaluates to True because Unset.bool returns False, so it raises ValueError("Access token is not set and signature cannot be generated!"). Sandboxes connected via connect() with secure=False cannot use file download or upload URL generation, while the equivalent create(secure=False) path works correctly.

Step-by-step proof:

  1. User calls Sandbox.connect(sandbox_id) where sandbox was created with secure=False.
  2. _cls_connect_sandbox receives sandbox where sandbox.envd_access_token is UNSET.
  3. Header guard works correctly: isinstance check prevents X-Access-Token from being set.
  4. BUT cls(..., envd_access_token=envd_access_token, ...) is called with envd_access_token=UNSET.
  5. SandboxBase.init stores self.__envd_access_token = UNSET.
  6. User calls sandbox.files.download_url("/file") — use_signature = UNSET is not None — True.
  7. get_signature(UNSET) is called — if not UNSET: — if not False: — True — raises ValueError.

Fix: Apply the same isinstance-str sanitization used in _create_sandbox:
envd_access_token = sandbox.envd_access_token if isinstance(sandbox.envd_access_token, str) else None
traffic_access_token = sandbox.traffic_access_token if isinstance(sandbox.traffic_access_token, str) else None

This is a pre-existing issue not introduced by this PR, but since the PR directly modifies _cls_connect_sandbox in both sync and async paths, this is the right moment to address the gap.

In _cls_connect_sandbox, envd_access_token, traffic_access_token, and
domain from the API response were passed directly to the constructor
without isinstance(str) cleaning. This could cause UNSET sentinel
values to leak into SandboxBase, leading to invalid signatures in
download_url()/upload_url() and incorrect domain URL construction.

Apply the same isinstance(str) sanitization pattern used in
_create_sandbox to ensure UNSET values are converted to None.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants