Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/olive-beers-obey.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 8 additions & 2 deletions packages/js-sdk/src/sandbox/sandboxApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ClientFactory,
ConnectionConfig,
ConnectionOpts,
DEFAULT_SANDBOX_TIMEOUT_MS,
} from '../connectionConfig'
import { compareVersions } from 'compare-versions'
import { ALL_TRAFFIC } from './network'
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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),
Expand Down
6 changes: 4 additions & 2 deletions packages/js-sdk/tests/sandbox/apiDefaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})

Expand Down
6 changes: 3 additions & 3 deletions packages/python-sdk/e2b/sandbox_async/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion packages/python-sdk/e2b/sandbox_async/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
)
Expand Down
6 changes: 3 additions & 3 deletions packages/python-sdk/e2b/sandbox_sync/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion packages/python-sdk/e2b/sandbox_sync/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
)
Expand Down
14 changes: 10 additions & 4 deletions packages/python-sdk/tests/shared/sandbox/test_api_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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


Expand Down
Loading