Skip to content

Commit 742a9ba

Browse files
authored
Update connect method (#1001)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Use new connect API for JS/Python SDKs to connect and only extend (never shorten) running sandbox timeouts, with updated types/docs and tests. > > - **SDKs**: > - **JS**: > - Replace `resume`/`setTimeout` flow with `SandboxApi.connectSandbox` calling `POST /sandboxes/{sandboxID}/connect`. > - Update `Sandbox.connect` (static/instance) to use returned `sandboxDomain`, `envdAccessToken`, `envdVersion`. > - Redefine `SandboxConnectOpts` to include `timeoutMs` that only extends existing timeout. > - **Python (async/sync)**: > - Introduce `_cls_connect` using `post_sandboxes_sandbox_id_connect` with `ConnectSandbox`; return `Sandbox` model. > - Update `connect` methods to use API response for domain/token/version; remove extra info fetch. > - Docstrings clarify timeout only extends for running sandboxes. > - **Tests**: > - Add cases ensuring connect does not shorten timeout and does extend when longer (JS, async Python, sync Python). > - Preserve error on connecting to non-running sandbox. > - **Meta**: > - Changeset: minor releases for `@e2b/python-sdk`, `e2b`, and `@e2b/cli`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e76b079. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 423d87c commit 742a9ba

File tree

10 files changed

+237
-135
lines changed

10 files changed

+237
-135
lines changed

.changeset/serious-badgers-jump.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@e2b/python-sdk': minor
3+
'e2b': minor
4+
'@e2b/cli': minor
5+
---
6+
7+
Update connect to only extend timeout

packages/js-sdk/src/sandbox/index.ts

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -404,29 +404,14 @@ export class Sandbox extends SandboxApi {
404404
sandboxId: string,
405405
opts?: SandboxConnectOpts
406406
): Promise<InstanceType<S>> {
407-
try {
408-
await SandboxApi.setTimeout(
409-
sandboxId,
410-
opts?.timeoutMs || DEFAULT_SANDBOX_TIMEOUT_MS,
411-
opts
412-
)
413-
} catch (e) {
414-
if (e instanceof SandboxError) {
415-
await SandboxApi.resumeSandbox(sandboxId, opts)
416-
} else {
417-
throw e
418-
}
419-
}
420-
421-
const info = await SandboxApi.getFullInfo(sandboxId, opts)
422-
407+
const sandbox = await SandboxApi.connectSandbox(sandboxId, opts)
423408
const config = new ConnectionConfig(opts)
424409

425410
return new this({
426411
sandboxId,
427-
sandboxDomain: info.sandboxDomain,
428-
envdAccessToken: info.envdAccessToken,
429-
envdVersion: info.envdVersion,
412+
sandboxDomain: sandbox.sandboxDomain,
413+
envdAccessToken: sandbox.envdAccessToken,
414+
envdVersion: sandbox.envdVersion,
430415
...config,
431416
}) as InstanceType<S>
432417
}
@@ -451,15 +436,7 @@ export class Sandbox extends SandboxApi {
451436
* ```
452437
*/
453438
async connect(opts?: SandboxBetaCreateOpts): Promise<this> {
454-
try {
455-
await SandboxApi.setTimeout(
456-
this.sandboxId,
457-
opts?.timeoutMs || DEFAULT_SANDBOX_TIMEOUT_MS,
458-
opts
459-
)
460-
} catch (e) {
461-
await SandboxApi.resumeSandbox(this.sandboxId, opts)
462-
}
439+
await SandboxApi.connectSandbox(this.sandboxId, opts)
463440

464441
return this
465442
}

packages/js-sdk/src/sandbox/sandboxApi.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,16 @@ export type SandboxBetaCreateOpts = SandboxOpts & {
103103
/**
104104
* Options for connecting to a Sandbox.
105105
*/
106-
export type SandboxConnectOpts = Omit<SandboxOpts, 'metadata' | 'envs'>
106+
export type SandboxConnectOpts = ConnectionOpts & {
107+
/**
108+
* Timeout for the sandbox in **milliseconds**.
109+
* For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
110+
* 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.
111+
*
112+
* @default 300_000 // 5 minutes
113+
*/
114+
timeoutMs?: number
115+
}
107116

108117
/**
109118
* State of the sandbox.
@@ -468,12 +477,7 @@ export class SandboxApi {
468477
template: string,
469478
timeoutMs: number,
470479
opts?: SandboxBetaCreateOpts
471-
): Promise<{
472-
sandboxId: string
473-
sandboxDomain?: string
474-
envdVersion: string
475-
envdAccessToken?: string
476-
}> {
480+
) {
477481
const config = new ConnectionConfig(opts)
478482
const client = new ApiClient(config)
479483

@@ -512,16 +516,16 @@ export class SandboxApi {
512516
}
513517
}
514518

515-
protected static async resumeSandbox(
519+
protected static async connectSandbox(
516520
sandboxId: string,
517521
opts?: SandboxConnectOpts
518-
): Promise<boolean> {
522+
) {
519523
const timeoutMs = opts?.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS
520524

521525
const config = new ConnectionConfig(opts)
522526
const client = new ApiClient(config)
523527

524-
const res = await client.api.POST('/sandboxes/{sandboxID}/resume', {
528+
const res = await client.api.POST('/sandboxes/{sandboxID}/connect', {
525529
params: {
526530
path: {
527531
sandboxID: sandboxId,
@@ -537,17 +541,17 @@ export class SandboxApi {
537541
throw new NotFoundError(`Paused sandbox ${sandboxId} not found`)
538542
}
539543

540-
if (res.error?.code === 409) {
541-
// Sandbox is already running
542-
return false
543-
}
544-
545544
const err = handleApiError(res)
546545
if (err) {
547546
throw err
548547
}
549548

550-
return true
549+
return {
550+
sandboxId: res.data!.sandboxID,
551+
sandboxDomain: res.data!.domain || undefined,
552+
envdVersion: res.data!.envdVersion,
553+
envdAccessToken: res.data!.envdAccessToken,
554+
}
551555
}
552556
}
553557

packages/js-sdk/tests/sandbox/connect.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,54 @@ sandboxTest.skipIf(isDebug)(
3535
)
3636
}
3737
)
38+
39+
test.skipIf(isDebug)(
40+
'connect does not shorten timeout on running sandbox',
41+
async () => {
42+
// Create sandbox with a 300 second timeout
43+
const sbx = await Sandbox.create(template, { timeoutMs: 300_000 })
44+
45+
try {
46+
const isRunning = await sbx.isRunning()
47+
assert.isTrue(isRunning)
48+
49+
// Get initial info to check endAt
50+
const infoBefore = await Sandbox.getInfo(sbx.sandboxId)
51+
52+
// Connect with a shorter timeout (10 seconds)
53+
await Sandbox.connect(sbx.sandboxId, { timeoutMs: 10_000 })
54+
55+
// Get info after connection
56+
const infoAfter = await sbx.getInfo()
57+
58+
// The endAt time should not have been shortened. It should be the same
59+
assert.equal(
60+
infoAfter.endAt.getTime(),
61+
infoBefore.endAt.getTime(),
62+
`Timeout was shortened: before=${infoBefore.endAt.toISOString()}, after=${infoAfter.endAt.toISOString()}`
63+
)
64+
} finally {
65+
await sbx.kill()
66+
}
67+
}
68+
)
69+
70+
sandboxTest.skipIf(isDebug)(
71+
'connect extends timeout on running sandbox',
72+
async ({ sandbox }) => {
73+
// Get initial info to check endAt
74+
const infoBefore = await sandbox.getInfo()
75+
76+
// Connect with a longer timeout
77+
await sandbox.connect({ timeoutMs: 600_000 })
78+
79+
// Get info after connection
80+
const infoAfter = await sandbox.getInfo()
81+
82+
// The endAt time should have been extended
83+
assert.isTrue(
84+
infoAfter.endAt.getTime() > infoBefore.endAt.getTime(),
85+
`Timeout was not extended: before=${infoBefore.endAt.toISOString()}, after=${infoAfter.endAt.toISOString()}`
86+
)
87+
}
88+
)

packages/python-sdk/e2b/sandbox_async/main.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ async def connect(
228228
With sandbox ID you can connect to the same sandbox from different places or environments (serverless functions, etc).
229229
230230
:param timeout: Timeout for the sandbox in **seconds**
231+
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
231232
:return: A running sandbox instance
232233
233234
@example
@@ -257,6 +258,7 @@ async def connect(
257258
258259
:param sandbox_id: Sandbox ID
259260
:param timeout: Timeout for the sandbox in **seconds**
261+
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
260262
:return: A running sandbox instance
261263
262264
@example
@@ -283,6 +285,7 @@ async def connect(
283285
With sandbox ID you can connect to the same sandbox from different places or environments (serverless functions, etc).
284286
285287
:param timeout: Timeout for the sandbox in **seconds**
288+
For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
286289
:return: A running sandbox instance
287290
288291
@example
@@ -294,7 +297,7 @@ async def connect(
294297
same_sandbox = await sandbox.connect()
295298
```
296299
"""
297-
await SandboxApi._cls_resume(
300+
await SandboxApi._cls_connect(
298301
sandbox_id=self.sandbox_id,
299302
timeout=timeout,
300303
**opts,
@@ -650,16 +653,14 @@ async def _cls_connect(
650653
timeout: Optional[int] = None,
651654
**opts: Unpack[ApiParams],
652655
) -> Self:
653-
await SandboxApi._cls_resume(
656+
sandbox = await SandboxApi._cls_connect(
654657
sandbox_id=sandbox_id,
655658
timeout=timeout,
656659
**opts,
657660
)
658661

659-
response = await SandboxApi._cls_get_info(sandbox_id, **opts)
660-
661662
sandbox_headers = {}
662-
envd_access_token = response._envd_access_token
663+
envd_access_token = sandbox.envd_access_token
663664
if envd_access_token is not None and not isinstance(envd_access_token, Unset):
664665
sandbox_headers["X-Access-Token"] = envd_access_token
665666

@@ -669,9 +670,9 @@ async def _cls_connect(
669670
)
670671

671672
return cls(
672-
sandbox_id=response.sandbox_id,
673-
sandbox_domain=response.sandbox_domain,
674-
envd_version=Version(response.envd_version),
673+
sandbox_id=sandbox.sandbox_id,
674+
sandbox_domain=sandbox.domain,
675+
envd_version=Version(sandbox.envd_version),
675676
envd_access_token=envd_access_token,
676677
connection_config=connection_config,
677678
)

packages/python-sdk/e2b/sandbox_async/sandbox_api.py

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
NewSandbox,
1414
PostSandboxesSandboxIDTimeoutBody,
1515
Error,
16-
ResumedSandbox,
16+
ConnectSandbox,
17+
Sandbox,
1718
)
1819
from e2b.api.client.api.sandboxes import (
1920
get_sandboxes_sandbox_id,
@@ -22,7 +23,7 @@
2223
post_sandboxes,
2324
get_sandboxes_sandbox_id_metrics,
2425
post_sandboxes_sandbox_id_pause,
25-
post_sandboxes_sandbox_id_resume,
26+
post_sandboxes_sandbox_id_connect,
2627
)
2728
from e2b.connection_config import ConnectionConfig, ApiParams
2829
from e2b.api import handle_api_exception
@@ -282,52 +283,42 @@ async def _cls_pause(
282283
if res.status_code >= 300:
283284
raise handle_api_exception(res)
284285

286+
# Check if res.parse is Error
287+
if isinstance(res.parsed, Error):
288+
raise SandboxException(f"{res.parsed.message}: Request failed")
289+
285290
return sandbox_id
286291

287292
@classmethod
288-
async def _cls_resume(
293+
async def _cls_connect(
289294
cls,
290295
sandbox_id: str,
291296
timeout: Optional[int] = None,
292297
**opts: Unpack[ApiParams],
293-
) -> bool:
298+
) -> Sandbox:
294299
timeout = timeout or SandboxBase.default_sandbox_timeout
295300

296-
# Temporary solution (02/12/2025),
297-
# Options discussed:
298-
# 1. No set - never sure how long the sandbox will be running
299-
# 2. Always set the timeout in code - the user can't just connect to the sandbox
300-
# without changing the timeout, round trip to the server time
301-
# 3. Set the timeout in resume on backend - side effect on error
302-
# 4. Create new endpoint for connect
303-
try:
304-
await SandboxApi._cls_set_timeout(
305-
sandbox_id=sandbox_id,
306-
timeout=timeout,
307-
**opts,
301+
# Sandbox is not running, resume it
302+
config = ConnectionConfig(**opts)
303+
304+
async with AsyncApiClient(
305+
config,
306+
limits=SandboxBase._limits,
307+
) as api_client:
308+
res = await post_sandboxes_sandbox_id_connect.asyncio_detailed(
309+
sandbox_id,
310+
client=api_client,
311+
body=ConnectSandbox(timeout=timeout),
308312
)
309-
return False
310-
except SandboxException:
311-
# Sandbox is not running, resume it
312-
config = ConnectionConfig(**opts)
313-
314-
async with AsyncApiClient(
315-
config,
316-
limits=SandboxBase._limits,
317-
) as api_client:
318-
res = await post_sandboxes_sandbox_id_resume.asyncio_detailed(
319-
sandbox_id,
320-
client=api_client,
321-
body=ResumedSandbox(timeout=timeout),
322-
)
323313

324-
if res.status_code == 404:
325-
raise NotFoundException(f"Paused sandbox {sandbox_id} not found")
314+
if res.status_code == 404:
315+
raise NotFoundException(f"Paused sandbox {sandbox_id} not found")
326316

327-
if res.status_code == 409:
328-
return False
317+
if res.status_code >= 300:
318+
raise handle_api_exception(res)
329319

330-
if res.status_code >= 300:
331-
raise handle_api_exception(res)
320+
# Check if res.parse is Error
321+
if isinstance(res.parsed, Error):
322+
raise SandboxException(f"{res.parsed.message}: Request failed")
332323

333-
return True
324+
return res.parsed

0 commit comments

Comments
 (0)