Repository navigation
fix(sdk): clean up sandbox when MCP gateway startup fails - #1548
Conversation
## Problem Fixes #1498. `Sandbox.create` allocates a remote sandbox before starting `mcp-gateway`. If gateway startup returns a non-zero exit code or throws an exception, creation fails before the sandbox object is returned. As a result, the caller has no sandbox ID to clean up, and the orphaned sandbox continues consuming resources until it times out. This state transition exists in synchronous Python, asynchronous Python, and JavaScript/TypeScript. ## Changes - Add a rollback boundary around MCP gateway startup in all three SDK implementations. - Attempt to kill the newly allocated sandbox when gateway startup fails. - Keep cleanup best-effort so a failed or cancelled `kill` does not replace the original gateway error. - Add real integration coverage for synchronous Python, asynchronous Python, and TypeScript. - Add a patch changeset for `e2b` and `@e2b/python-sdk`. ## Usage Behavior No API changes are required. Existing code continues to receive the original startup error, but a failed creation will normally no longer leave a sandbox behind. ## Validation Integration tests have been added and verified successfully. ## Notes PR #1499 is a friendly competing implementation for the same issue. The maintainer requested that its mocked tests be replaced with real integration tests, but the PR received no further updates for several days. The maintainer subsequently confirmed that an alternative contribution could be submitted as long as it was implemented independently from scratch, so this PR was prepared only after that confirmation.
🦋 Changeset detectedLatest commit: 987aeae The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 SummaryLow Risk Overview JS, sync Python, and async Python share the same rollback: best-effort Reviewed by Cursor Bugbot for commit 987aeae. Bugbot is set up for automated code reviews on this repo. Configure here. |
Package ArtifactsBuilt from f704c5d. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-2.38.3-mcp-gateway-startup-cleanup.0.tgzCLI ( npm install ./e2b-cli-2.16.2-mcp-gateway-startup-cleanup.0.tgzPython SDK ( pip install ./e2b-2.37.1+mcp.gateway.startup.cleanup-py3-none-any.whl |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 16390cfd76
ℹ️ 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".
There was a problem hiding this comment.
Beyond the inline finding on the dead MCP gateway exit-code check, I looked at the exception-handling in the new rollback blocks themselves: whether the except BaseException wrapping kill() could swallow a second KeyboardInterrupt/SystemExit raised during cleanup, and whether the blocking kill() call in the async path could delay CancelledError propagation past a wait_for/timeout deadline. Both are inherent to the best-effort cleanup design here (the PR explicitly swallows cleanup failures so they don't mask the original error) rather than distinct bugs, so I'm not flagging them separately.
Extended reasoning...
Checked the sync and async except BaseException: ... finally raise rollback blocks in sandbox_sync/main.py and sandbox_async/main.py for interrupt-swallowing and cancellation-delay issues beyond the inline finding. The inner except BaseException: pass around kill() only suppresses exceptions from the cleanup call itself and still re-raises the original error afterward, and the blocking kill() during cleanup is bounded and best-effort by design, so these did not rise to separate bugs worth blocking on.
The tests relied on `mcp: {invalid_server: {}}` making mcp-gateway exit
non-zero, but the gateway accepts unknown server names and starts fine,
so Sandbox.create succeeded and the expected rejection never happened.
Pin the tests to the base template, which has no mcp-gateway binary, so
gateway startup genuinely fails after the sandbox is allocated. The
failure now surfaces as CommandExitError/CommandExitException rather
than the exit-code message, so match on the exception type instead.
Co-Authored-By: Claude Fable 5 <[email protected]>
…preserve cancellation Address review findings on the rollback block in all three SDKs: - The manual exit-code check was dead code: foreground commands.run already throws CommandExitError/CommandExitException on non-zero exit, so the documented "Failed to start MCP gateway" message was never produced. Catch the command exit error and re-raise it as SandboxError/SandboxException with that message and the stderr. - In async Python, re-raise asyncio.CancelledError from the best-effort kill() so caller cancellation (e.g. asyncio.timeout) is honored instead of being swallowed; suppress only ordinary cleanup failures in the sync path too. - Tests assert the wrapped message again, which is now genuinely reachable. Co-Authored-By: Claude Fable 5 <[email protected]>
Problem
Fixes #1498.
Sandbox.createallocates a remote sandbox before startingmcp-gateway. If gateway startup fails, creation throws before the sandbox object is returned. As a result, the caller has no sandbox ID to clean up, and the orphaned sandbox continues consuming resources until it times out.This state transition exists in synchronous Python, asynchronous Python, and JavaScript/TypeScript.
Changes
SandboxError(JS) /SandboxException(Python) with aFailed to start MCP gateway: <stderr>message. Previously the intended message was unreachable dead code — foregroundcommands.runalready throws on non-zero exit — so callers got a bareCommandExitError/CommandExitException.asyncio.CancelledErrorfrom the best-effortkill()so caller cancellation (e.g.asyncio.timeout) is honored; only ordinary cleanup failures are suppressed and never mask the original error.mcp-gatewaybinary) so gateway startup genuinely fails after allocation.e2band@e2b/python-sdk.Usage Behavior
No API changes. A failed creation no longer leaves a sandbox behind, and the error is now descriptive:
Validation
All three integration tests verified against real infra: creation rejects with the documented error and no sandbox remains.
Notes
Supersedes #1547 by @hxaxd (squash-merged into this branch to preserve attribution).
🤖 Generated with Claude Code