From 37eca004a7a2c04cf1e3a6bf5f244199dd541a5d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 25 Sep 2026 21:14:52 +0000 Subject: [PATCH] fix(sdk): keep the 5-minute default timeout on sandbox fork The fork endpoint has no v2 route, so an omitted timeout falls back to its own 15-second default instead of the 300 seconds create and connect get from /v2. Send the documented 5 minutes when the caller omits one. Co-authored-by: Rauf Guliyev --- .changeset/olive-beers-obey.md | 6 ++++++ packages/js-sdk/src/sandbox/sandboxApi.ts | 10 ++++++++-- packages/js-sdk/tests/sandbox/apiDefaults.test.ts | 6 ++++-- packages/python-sdk/e2b/sandbox_async/main.py | 6 +++--- .../python-sdk/e2b/sandbox_async/sandbox_api.py | 10 +++++++++- packages/python-sdk/e2b/sandbox_sync/main.py | 6 +++--- .../python-sdk/e2b/sandbox_sync/sandbox_api.py | 10 +++++++++- .../tests/shared/sandbox/test_api_defaults.py | 14 ++++++++++---- 8 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 .changeset/olive-beers-obey.md diff --git a/.changeset/olive-beers-obey.md b/.changeset/olive-beers-obey.md new file mode 100644 index 0000000000..73ad609778 --- /dev/null +++ b/.changeset/olive-beers-obey.md @@ -0,0 +1,6 @@ +--- +'@e2b/python-sdk': patch +'e2b': patch +--- + +Keep sending the documented 5-minute timeout when `Sandbox.fork()` / `fork()` is called without one. Sandbox create and connect moved to the v2 endpoints, which default an omitted timeout to 300 seconds, but fork has no v2 route and its own default is 15 seconds — so since 2.51.0 forks created without an explicit timeout expired (or auto-paused, when the sandbox was configured that way) about fifteen seconds after they started. An explicit `timeoutMs` / `timeout` is still sent unchanged. diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index c144863534..9608a65087 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -3,6 +3,7 @@ import { ClientFactory, ConnectionConfig, ConnectionOpts, + DEFAULT_SANDBOX_TIMEOUT_MS, } from '../connectionConfig' import { compareVersions } from 'compare-versions' import { ALL_TRAFFIC } from './network' @@ -548,6 +549,8 @@ export interface SandboxForkOpts extends ConnectionOpts { /** * Timeout for the forked sandboxes in **milliseconds**. * Maximum time a sandbox can be kept alive is 24 hours (86_400_000 milliseconds) for Pro users and 1 hour (3_600_000 milliseconds) for Hobby users. + * + * @default 300_000 // 5 minutes */ timeoutMs?: number } @@ -1773,8 +1776,11 @@ export class SandboxApi extends ClientFactory { }, }, body: { - timeout: - timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), + // Unlike create and connect, the fork endpoint has no v2 route: an + // omitted timeout falls back to the legacy 15-second default, which is + // not a usable sandbox lifetime. Send the 5 minutes the SDK documents + // until the API's own fork default matches it. + timeout: timeoutToSeconds(timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS), count, }, signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), diff --git a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts index 91459cc6d7..d2c59c4c85 100644 --- a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts +++ b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts @@ -84,11 +84,13 @@ test('Sandbox.create sends explicit timeout and allow_internet_access', async () expect(lastCreateBody?.allow_internet_access).toBe(false) }) -test('Sandbox.fork omits timeout and count when unset', async () => { +test('Sandbox.fork sends the 5-minute timeout and omits count when unset', async () => { await Sandbox.fork('test-sandbox-id', { apiKey: TEST_API_KEY }) expect(lastForkBody).toBeDefined() - expect(lastForkBody).not.toHaveProperty('timeout') + // The fork endpoint's own default is 15 seconds, so the SDK keeps sending + // the 5 minutes it documents. + expect(lastForkBody?.timeout).toBe(300) expect(lastForkBody).not.toHaveProperty('count') }) diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index a5eac92642..e53b69110d 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -413,7 +413,7 @@ async def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**. + :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds. :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -452,7 +452,7 @@ async def fork( (e.g. 429 to `RateLimitException`). :param sandbox_id: Sandbox ID - :param timeout: Timeout for the forked sandboxes in **seconds**. + :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds. :param count: Number of forked sandboxes to create. :param logger: Logger used for request and response logging for the forked sandboxes. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. @@ -489,7 +489,7 @@ async def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**. + :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds. :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index ca0de64176..01923f84ad 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -451,8 +451,16 @@ async def _cls_fork( res = await post_sandboxes_sandbox_id_fork.asyncio_detailed( sandbox_id, client=api_client, + # Unlike create and connect, the fork endpoint has no v2 route: an + # omitted timeout falls back to the legacy 15-second default, which + # is not a usable sandbox lifetime. Send the 5 minutes the SDK + # documents until the API's own fork default matches it. body=SandboxForkRequest( - timeout=timeout if timeout is not None else UNSET, + timeout=( + timeout + if timeout is not None + else SandboxBase.default_sandbox_timeout + ), count=count if count is not None else UNSET, ), ) diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 5dfae376fe..70bafbcf9e 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -408,7 +408,7 @@ def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**. + :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds. :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -447,7 +447,7 @@ def fork( (e.g. 429 to `RateLimitException`). :param sandbox_id: Sandbox ID - :param timeout: Timeout for the forked sandboxes in **seconds**. + :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds. :param count: Number of forked sandboxes to create. :param logger: Logger used for request and response logging for the forked sandboxes. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. @@ -484,7 +484,7 @@ def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**. + :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds. :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 07f65f5e3f..27fc6122c5 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -405,8 +405,16 @@ def _cls_fork( res = post_sandboxes_sandbox_id_fork.sync_detailed( sandbox_id, client=api_client, + # Unlike create and connect, the fork endpoint has no v2 route: an + # omitted timeout falls back to the legacy 15-second default, which + # is not a usable sandbox lifetime. Send the 5 minutes the SDK + # documents until the API's own fork default matches it. body=SandboxForkRequest( - timeout=timeout if timeout is not None else UNSET, + timeout=( + timeout + if timeout is not None + else SandboxBase.default_sandbox_timeout + ), count=count if count is not None else UNSET, ), ) diff --git a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py index 1ab0b5779f..de6c1d6403 100644 --- a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py +++ b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py @@ -114,10 +114,14 @@ async def _async_fork_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any return request.call_args.kwargs["body"].to_dict() -def test_fork_omits_timeout_and_count_when_unset(monkeypatch, test_api_key): +def test_fork_sends_the_five_minute_timeout_and_omits_count_when_unset( + monkeypatch, test_api_key +): body = _sync_fork_body(monkeypatch, test_api_key) - assert "timeout" not in body + # The fork endpoint's own default is 15 seconds, so the SDK keeps sending + # the 5 minutes it documents. + assert body["timeout"] == 300 assert "count" not in body @@ -128,10 +132,12 @@ def test_fork_sends_explicit_timeout_and_count(monkeypatch, test_api_key): assert body["count"] == 2 -async def test_async_fork_omits_timeout_and_count_when_unset(monkeypatch, test_api_key): +async def test_async_fork_sends_the_five_minute_timeout_and_omits_count_when_unset( + monkeypatch, test_api_key +): body = await _async_fork_body(monkeypatch, test_api_key) - assert "timeout" not in body + assert body["timeout"] == 300 assert "count" not in body