Skip to content

feat(js-sdk): support AbortSignal for request cancellation#1328

Merged
mishushakov merged 12 commits into
mainfrom
mishushakov/js-sdk-abort-signal
May 15, 2026
Merged

feat(js-sdk): support AbortSignal for request cancellation#1328
mishushakov merged 12 commits into
mainfrom
mishushakov/js-sdk-abort-signal

Conversation

@mishushakov
Copy link
Copy Markdown
Member

@mishushakov mishushakov commented May 14, 2026

Summary

  • Adds an optional signal: AbortSignal to SDK request options across Sandbox, Commands, Pty, Filesystem, and Volume methods.
  • ConnectionConfig.getSignal() (and VolumeConnectionConfig.getSignal()) delegate to a shared buildRequestSignal() helper that combines the user signal with the existing request-timeout signal via AbortSignal.any().
  • Streaming RPCs (commands.run/connect, pty.create/connect, watchDir) route through a shared setupRequestController() that bridges the user signal into the internal AbortController and exposes clearStartTimeout + idempotent cleanup. Handshake timer is cleared after start succeeds (so long-running streams aren't aborted at requestTimeoutMs), and CommandHandle/WatchHandle call cleanup in a finally so the user-signal listener is always detached.
  • Handshake timeout aborts with DOMException('Request handshake timed out …', 'TimeoutError') so callers can distinguish from user aborts.
  • SandboxPaginator.nextItems / SnapshotPaginator.nextItems now accept a per-call SandboxApiOpts (incl. signal, apiKey, domain, headers, requestTimeoutMs); the per-call options are merged with the constructor opts (per-call values win) so passing e.g. { signal } does not drop the constructor's apiKey.
  • Python SDK parity: SandboxPaginator.next_items / SnapshotPaginator.next_items (sync + async) now accept **opts: Unpack[ApiParams] with the same merge semantics.
  • Bumps the CLI's TypeScript range to ^5.4.5 so its dom lib includes AbortSignal.any.

Fixes #1312

Usage:
```ts
const ctrl = new AbortController()
await Sandbox.create(template, { apiKey, signal: ctrl.signal })
await sandbox.commands.run('long-running', { signal: ctrl.signal })
await sandbox.files.write('/tmp/x', 'hi', { signal: ctrl.signal })

const paginator = Sandbox.list({ apiKey })
// per-call opts merge with constructor opts — apiKey is preserved:
await paginator.nextItems({ signal: ctrl.signal })
// override fields individually:
await paginator.nextItems({ apiKey: otherKey, signal: ctrl.signal })
```

```python

Python

paginator = Sandbox.list(api_key=api_key)
paginator.next_items(request_timeout=10.0) # api_key still applied
```

Test plan

  • `pnpm run typecheck` (js-sdk + cli)
  • `pnpm run lint`
  • `pnpm run format`
  • `pnpm exec vitest run tests/sandbox/abortSignal.test.ts tests/connectionConfig.test.ts` (19/19 pass — covers `Sandbox.create`/`kill`, paginator per-call signal, `getSignal`, `setupRequestController` lifecycle, `TimeoutError` reason)
  • `poetry run make typecheck` / `make lint` (python-sdk)

🤖 Generated with Claude Code

Add an optional `signal: AbortSignal` to SDK request options across
`Sandbox`, `Commands`, `Pty`, `Filesystem`, and `Volume` methods. The
user signal is combined with the existing request-timeout signal via
`AbortSignal.any()`, and is wired into the manual AbortControllers used
by streaming RPCs (commands.run/connect, pty.create/connect, watchDir).

Fixes #1312
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 14, 2026

🦋 Changeset detected

Latest commit: c653240

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
e2b Minor

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

@cursor
Copy link
Copy Markdown

cursor Bot commented May 14, 2026

PR Summary

Medium Risk
Widely changes SDK request option types and abort/timeout behavior across many methods, which can cause breaking runtime differences in long-lived streams and pagination calls if misused. Added tests reduce risk but cancellation semantics are easy to get wrong across environments.

Overview
JS SDK methods now accept signal: AbortSignal and wire it through to underlying HTTP/RPC calls, including combining it with request timeouts and adding a shared streaming controller with deterministic cleanup.

SandboxPaginator/SnapshotPaginator now take per-call SandboxApiOpts overrides (including signal) instead of being locked to construction-time options, and the Python SDK paginators (sync/async) add the same per-call ApiParams overrides. CLI TypeScript is bumped to support AbortSignal.any, and new tests cover abort/timeout/controller behavior.

Reviewed by Cursor Bugbot for commit c653240. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba388391b1

ℹ️ 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 thread packages/js-sdk/src/sandbox/sandboxApi.ts
Comment thread packages/js-sdk/src/sandbox/commands/index.ts Outdated
Comment thread packages/js-sdk/src/sandbox/commands/index.ts
The CLI typechecks the js-sdk source via path mapping with
\`lib: [es2022, dom, dom.iterable]\` and TypeScript 5.2, which does not
expose \`AbortSignal.any\`. Replace it with a small \`combineAbortSignals\`
helper so the type is portable across consumer tsconfigs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread packages/js-sdk/src/sandbox/sandboxApi.ts
Comment thread packages/js-sdk/src/sandbox/commands/index.ts Outdated
Comment thread packages/js-sdk/src/sandbox/commands/index.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 14, 2026

Package Artifacts

Built from fe9ee6d. Download artifacts from this workflow run.

JS SDK (e2b@2.20.2-mishushakov-js-sdk-abort-signal.0):

npm install ./e2b-2.20.2-mishushakov-js-sdk-abort-signal.0.tgz

CLI (@e2b/cli@2.10.2-mishushakov-js-sdk-abort-signal.0):

npm install ./e2b-cli-2.10.2-mishushakov-js-sdk-abort-signal.0.tgz

Python SDK (e2b==2.21.1+mishushakov-js-sdk-abort-signal):

pip install ./e2b-2.21.1+mishushakov.js.sdk.abort.signal-py3-none-any.whl

mishushakov and others added 3 commits May 14, 2026 19:59
CLI was pinned to TypeScript 5.2.2 whose dom lib does not declare
\`AbortSignal.any\`, causing typecheck failures when the CLI imports
the js-sdk source via path mapping. Align with the js-sdk's TS range.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Propagate signal through SandboxPaginator/SnapshotPaginator so
  Sandbox.list({signal}) / listSnapshots({signal}) actually cancel.
- Extract setupRequestController() in connectionConfig to centralize
  user-signal + timeout wiring with an idempotent cleanup() that
  detaches the listener, clears the timer, and aborts the controller.
- Use it in Commands.start/connect, Pty.create/connect, and
  Filesystem.watchDir, so the listener is always cleaned up — including
  the stdin version-check error path (now thrown before setup) and the
  initial startup catch.
- Have CommandHandle.handleEvents and WatchHandle.handleEvents call
  handleDisconnect/handleStop in a finally block so the listener is
  also released on natural stream completion (e.g. commands.run + wait)
  or after kill().
- Add tests for paginator cancellation and setupRequestController.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread packages/js-sdk/src/connectionConfig.ts Outdated
mishushakov and others added 5 commits May 15, 2026 17:47
The previous refactor kept the requestTimeoutMs timer running for the
entire stream lifetime, which would prematurely abort any command,
PTY session, or watchDir running longer than the request timeout
(default 60s). The timer is only meant to bound the initial
handshake — restore the old behaviour by splitting cleanup into
clearStartTimeout (called once handleProcessStartEvent /
handleWatchDirStartEvent resolves) and cleanup (called at stream
end or on startup failure). The user-signal listener stays attached
for the full stream lifetime so callers can still abort long-running
streams.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extract shared buildRequestSignal() from ConnectionConfig.getSignal
  and VolumeConnectionConfig.getSignal so the two implementations
  can't drift.
- setupRequestController now aborts with
  DOMException('Request handshake timed out ...', 'TimeoutError') so
  callers can distinguish handshake timeouts from user aborts (mirrors
  AbortSignal.timeout() semantics).
- Document on SandboxListOpts.signal and SnapshotListOpts.signal that
  the signal is stored on the paginator and applies to every
  subsequent nextItems() call (construct a new paginator for a fresh
  signal).
- Drop the defensive try/catch around handleDisconnect/handleStop in
  CommandHandle and WatchHandle — both wrap an idempotent cleanup, so
  the silent catch hides nothing today and would mask real bugs if
  those methods ever grow side effects.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the constructor-time signal on SandboxPaginator/SnapshotPaginator
with a per-call AbortSignal accepted by nextItems({ signal }). Storing
the signal on the paginator was footgunny: one abort would poison every
subsequent page. Per-call signals make cancellation explicit and let the
caller mix cancellable and non-cancellable pages on the same paginator.

Also Omit 'signal' from SandboxListOpts/SnapshotListOpts since the
paginator constructor performs no I/O and storing it had no effect.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…flake

The tests previously used `setTimeout(() => controller.abort(), 25)` to
fire the abort after the request had (presumably) started. On Windows CI
that 25ms guess sometimes landed before MSW invoked the handler, so the
handler attached its `abort` listener to an already-aborted signal and
hung until the 30s vitest timeout. Wait on MSW's `request:start` event
instead, and keep the handler race-safe by checking `signal.aborted`
before subscribing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Paginator no longer eagerly constructs a ConnectionConfig/ApiClient.
Instead it stores the constructor opts and builds a fresh config + client
inside each nextItems call. nextItems now accepts a per-call opts argument
that fully overrides the stored connection options for that single page
request (matching the all-or-nothing semantics in JS via `opts ?? this.opts`,
and `opts if opts else self._opts` in Python).

Synced across both JS (SandboxApiOpts, sync) and Python (ApiParams, sync +
async) SDKs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Stale comment

Comment thread packages/js-sdk/src/sandbox/sandboxApi.ts Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 905cc47. Configure here.

Comment thread packages/js-sdk/src/sandbox/sandboxApi.ts Outdated
mishushakov and others added 2 commits May 15, 2026 21:15
Revert the storage change from the previous commit: paginators eagerly
build a ConnectionConfig in the constructor and store it (JS: this.config,
Python: self._config). nextItems still accepts a per-call opts argument
that fully overrides the stored config for one request, but the default
path doesn't pay to reconstruct the config every call.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-call opts on nextItems / next_items previously replaced the
paginator's stored connection options, silently dropping apiKey,
domain, headers, etc. set at construction time when callers passed
just a signal (or any single override). Switch to merge semantics:
new ConnectionConfig({ ...this.opts, ...opts }) in JS,
ConnectionConfig(**{**self._opts, **opts}) in Python (sync + async).

Reported in PR review on #1328.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@mishushakov mishushakov merged commit 2ac5de2 into main May 15, 2026
24 of 25 checks passed
@mishushakov mishushakov deleted the mishushakov/js-sdk-abort-signal branch May 15, 2026 21:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support AbortSignal in JS SDK methods for request cancellation

2 participants