Skip to content

Commit 4e16cff

Browse files
mishushakovclaude
andauthored
feat(js-sdk): add proxy connection param (#1386)
Adds a `proxy` connection parameter to the JS SDK, mirroring the Python SDK. When set, requests are routed through the given HTTP proxy via an undici `ProxyAgent` dispatcher (fetchers are cached per-proxy so non-proxy traffic is unaffected). It applies to control-plane API requests, all requests made to the returned sandbox (REST plus filesystem/commands/pty RPC), and volume requests. Behavior is unchanged when no proxy is provided, and unit tests cover both the API and envd fetch paths. ## Usage ```ts import { Sandbox } from 'e2b' // Routes API + all sandbox requests through the proxy const sandbox = await Sandbox.create({ proxy: 'http://user:pass@127.0.0.1:8080', }) await sandbox.files.write('/hello.txt', 'world') // Also works when connecting to an existing sandbox const sbx = await Sandbox.connect(sandboxId, { proxy: 'http://127.0.0.1:8080' }) ``` > Proxying relies on the optional `undici` package and the Node runtime; in browser/edge runtimes requests use global `fetch`, which has no proxy support (same as the existing HTTP/2 dispatcher). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 26ca87c commit 4e16cff

17 files changed

Lines changed: 331 additions & 35 deletions

File tree

.changeset/js-sdk-proxy-param.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"e2b": patch
3+
---
4+
5+
Add `proxy` connection parameter to route SDK requests through an HTTP proxy, matching the Python SDK. When set, it applies to API requests, all requests made to the returned sandbox, and volume requests.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@e2b/python-sdk": patch
3+
"e2b": patch
4+
---
5+
6+
Fix `proxy` not being applied to volume content requests. `Volume.create`/`Volume.connect` now store the `proxy` on the returned instance, so instance methods (`list`, `readFile`, `writeFile`, `makeDir`, `getInfo`, `updateMetadata`, `remove`, …) route through it without having to pass `proxy` on every call. A per-call `proxy` still takes precedence.

packages/js-sdk/src/api/http2.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@ const DEFAULT_API_CONNECTION_LIMIT = 100
1313
// Override via env if your workload needs different.
1414
const DEFAULT_API_INFLIGHT_LIMIT = 1000
1515

16-
let apiFetch: typeof fetch | undefined
16+
// Fetchers are cached per proxy so requests without a proxy keep sharing a
17+
// single dispatcher while each distinct proxy URL gets its own.
18+
const apiFetchers = new Map<string, typeof fetch>()
1719

18-
export function createApiFetch(): typeof fetch {
19-
if (apiFetch) {
20-
return apiFetch
20+
export function createApiFetch(proxy?: string): typeof fetch {
21+
const key = proxy ?? ''
22+
23+
const cached = apiFetchers.get(key)
24+
if (cached) {
25+
return cached
2126
}
2227

23-
apiFetch = createApiFetchForRuntime(runtime)
28+
const apiFetch = createApiFetchForRuntime(runtime, { proxy })
29+
apiFetchers.set(key, apiFetch)
2430

2531
return apiFetch
2632
}
@@ -30,6 +36,7 @@ export function createApiFetchForRuntime(
3036
options: {
3137
connectionLimit?: number
3238
inflightLimit?: number
39+
proxy?: string
3340
loadUndici?: () => Promise<UndiciModule | undefined>
3441
} = {}
3542
): typeof fetch {
@@ -50,6 +57,7 @@ export function createApiFetchForRuntime(
5057
async function buildApiFetcher(options: {
5158
connectionLimit?: number
5259
inflightLimit?: number
60+
proxy?: string
5361
loadUndici?: () => Promise<UndiciModule | undefined>
5462
}): Promise<typeof fetch> {
5563
const undici = await (options.loadUndici ?? loadUndici)()
@@ -59,11 +67,18 @@ async function buildApiFetcher(options: {
5967
return limitConcurrency(fetch, inflightLimit)
6068
}
6169

62-
const { Agent, fetch: undiciFetch } = undici
63-
const dispatcher = new Agent({
64-
allowH2: true,
65-
connections: options.connectionLimit ?? getApiConnectionLimit(),
66-
})
70+
const { Agent, ProxyAgent, fetch: undiciFetch } = undici
71+
const connections = options.connectionLimit ?? getApiConnectionLimit()
72+
const dispatcher = options.proxy
73+
? new ProxyAgent({
74+
uri: options.proxy,
75+
allowH2: true,
76+
connections,
77+
})
78+
: new Agent({
79+
allowH2: true,
80+
connections,
81+
})
6782
const fetchWithDispatcher = undiciFetch as unknown as (
6883
input: RequestInfo | URL,
6984
init?: UndiciRequestInit

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ class ApiClient {
9595

9696
this.api = createClient<paths>({
9797
baseUrl: config.apiUrl,
98-
fetch: createApiFetch(),
98+
fetch: createApiFetch(config.proxy),
9999
// In HTTP 1.1, all connections are considered persistent unless declared otherwise
100100
// keepalive: true,
101101
headers: {

packages/js-sdk/src/connectionConfig.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ export interface ConnectionOpts {
6969
*/
7070
headers?: Record<string, string>
7171

72+
/**
73+
* Proxy URL to use for requests. In case of a sandbox it applies to all
74+
* requests made to the returned sandbox.
75+
*
76+
* @example 'http://user:pass@127.0.0.1:8080'
77+
*/
78+
proxy?: string
79+
7280
/**
7381
* Additional headers to send with E2B API requests.
7482
*/
@@ -193,6 +201,8 @@ export class ConnectionConfig {
193201

194202
readonly headers?: Record<string, string>
195203

204+
readonly proxy?: string
205+
196206
constructor(opts?: ConnectionOpts) {
197207
this.apiKey = opts?.apiKey || ConnectionConfig.apiKey
198208
this.debug = opts?.debug || ConnectionConfig.debug
@@ -202,6 +212,7 @@ export class ConnectionConfig {
202212
this.logger = opts?.logger
203213
this.headers = { ...(opts?.headers ?? {}), ...(opts?.apiHeaders ?? {}) }
204214
this.headers['User-Agent'] = `e2b-js-sdk/${version}`
215+
this.proxy = opts?.proxy
205216

206217
this.apiUrl =
207218
opts?.apiUrl ||

packages/js-sdk/src/envd/http2.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import {
1111
type EnvdFetchOptions = {
1212
connectionLimit?: number
1313
inflightLimit?: number
14+
proxy?: string
1415
loadUndici?: () => Promise<UndiciModule | undefined>
1516
}
1617

17-
let envdFetch: typeof fetch | undefined
18-
let envdRpcFetch: typeof fetch | undefined
18+
// Fetchers are cached per proxy so requests without a proxy keep sharing a
19+
// single dispatcher while each distinct proxy URL gets its own.
20+
const envdFetchers = new Map<string, typeof fetch>()
21+
const envdRpcFetchers = new Map<string, typeof fetch>()
1922
const DEFAULT_ENVD_CONNECTION_LIMIT = 10
2023
const DEFAULT_ENVD_RPC_CONNECTION_LIMIT = 200
2124
const DEFAULT_ENVD_INFLIGHT_LIMIT = 2000
@@ -49,13 +52,19 @@ async function buildEnvdFetcher(
4952
return limitConcurrency(fetch, inflightLimit)
5053
}
5154

52-
const { Agent, fetch: undiciFetch } = undici
53-
const dispatcherOptions: { allowH2: true; connections?: number } = {
54-
allowH2: true,
55-
connections: options.connectionLimit ?? DEFAULT_ENVD_CONNECTION_LIMIT,
56-
}
57-
58-
const dispatcher = new Agent(dispatcherOptions)
55+
const { Agent, ProxyAgent, fetch: undiciFetch } = undici
56+
const connections = options.connectionLimit ?? DEFAULT_ENVD_CONNECTION_LIMIT
57+
58+
const dispatcher = options.proxy
59+
? new ProxyAgent({
60+
uri: options.proxy,
61+
allowH2: true,
62+
connections,
63+
})
64+
: new Agent({
65+
allowH2: true,
66+
connections,
67+
})
5968
const fetchWithDispatcher = undiciFetch as unknown as (
6069
input: RequestInfo | URL,
6170
init?: UndiciRequestInit
@@ -73,29 +82,39 @@ async function buildEnvdFetcher(
7382
return limitConcurrency(wrapped, inflightLimit)
7483
}
7584

76-
export function createEnvdFetch(): typeof fetch {
77-
if (envdFetch) {
78-
return envdFetch
85+
export function createEnvdFetch(proxy?: string): typeof fetch {
86+
const key = proxy ?? ''
87+
88+
const cached = envdFetchers.get(key)
89+
if (cached) {
90+
return cached
7991
}
8092

8193
// Keep one origin connection for short envd REST calls. If ALPN falls back
8294
// to h1, this favors connection pressure over per-sandbox throughput.
83-
envdFetch = createEnvdFetchForRuntime(runtime, {
95+
const envdFetch = createEnvdFetchForRuntime(runtime, {
8496
inflightLimit: getEnvdInflightLimit(),
97+
proxy,
8598
})
99+
envdFetchers.set(key, envdFetch)
86100

87101
return envdFetch
88102
}
89103

90-
export function createEnvdRpcFetch(): typeof fetch {
91-
if (envdRpcFetch) {
92-
return envdRpcFetch
104+
export function createEnvdRpcFetch(proxy?: string): typeof fetch {
105+
const key = proxy ?? ''
106+
107+
const cached = envdRpcFetchers.get(key)
108+
if (cached) {
109+
return cached
93110
}
94111

95-
envdRpcFetch = createEnvdFetchForRuntime(runtime, {
112+
const envdRpcFetch = createEnvdFetchForRuntime(runtime, {
96113
connectionLimit: getEnvdRpcConnectionLimit(),
97114
inflightLimit: getEnvdRpcInflightLimit(),
115+
proxy,
98116
})
117+
envdRpcFetchers.set(key, envdRpcFetch)
99118

100119
return envdRpcFetch
101120
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ export class Sandbox extends SandboxApi {
159159
'E2b-Sandbox-Id': this.sandboxId,
160160
'E2b-Sandbox-Port': this.envdPort.toString(),
161161
}
162-
const envdFetch = createEnvdFetch()
163-
const envdRpcFetch = createEnvdRpcFetch()
162+
const envdFetch = createEnvdFetch(this.connectionConfig.proxy)
163+
const envdRpcFetch = createEnvdRpcFetch(this.connectionConfig.proxy)
164164

165165
const rpcTransport = createConnectTransport({
166166
baseUrl: this.envdApiUrl,

packages/js-sdk/src/undici.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ export type UndiciRequestInit = RequestInit & {
55

66
export type UndiciModule = {
77
Agent: new (options: { allowH2: true; connections?: number }) => unknown
8+
ProxyAgent: new (options: {
9+
uri: string
10+
allowH2: true
11+
connections?: number
12+
}) => unknown
813
fetch: unknown
914
}
1015

packages/js-sdk/src/volume/client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import createClient from 'openapi-fetch'
22

33
import type { components, paths } from './schema.gen'
44
import { defaultHeaders, getEnvVar } from '../api/metadata'
5+
import { createApiFetch } from '../api/http2'
56
import { buildRequestSignal } from '../connectionConfig'
67
import { createApiLogger, Logger } from '../logs'
78
import type { Volume } from './index'
@@ -49,6 +50,13 @@ export interface VolumeApiOpts {
4950
*/
5051
headers?: Record<string, string>
5152

53+
/**
54+
* Proxy URL to use for requests.
55+
*
56+
* @example 'http://user:pass@127.0.0.1:8080'
57+
*/
58+
proxy?: string
59+
5260
/**
5361
* An optional `AbortSignal` that can be used to cancel the in-flight request.
5462
* When the signal is aborted, the underlying `fetch` is aborted and the
@@ -66,6 +74,7 @@ export class VolumeConnectionConfig {
6674
readonly logger?: Logger
6775
readonly requestTimeoutMs?: number
6876
readonly signal?: AbortSignal
77+
readonly proxy?: string
6978

7079
constructor(volume: Volume, opts?: VolumeApiOpts) {
7180
this.domain = opts?.domain || volume.domain || VolumeConnectionConfig.domain
@@ -79,6 +88,7 @@ export class VolumeConnectionConfig {
7988
this.logger = opts?.logger
8089
this.requestTimeoutMs = opts?.requestTimeoutMs
8190
this.signal = opts?.signal
91+
this.proxy = opts?.proxy || volume.proxy
8292
}
8393

8494
private static get domain() {
@@ -110,6 +120,7 @@ class VolumeApiClient {
110120
constructor(config: VolumeConnectionConfig) {
111121
this.api = createClient<paths>({
112122
baseUrl: config.apiUrl,
123+
fetch: createApiFetch(config.proxy),
113124
headers: {
114125
...defaultHeaders,
115126
...(config.token && { Authorization: `Bearer ${config.token}` }),

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export class Volume {
6565
*/
6666
readonly debug?: boolean
6767

68+
/**
69+
* Proxy URL used for requests to the volume content API.
70+
*/
71+
readonly proxy?: string
72+
6873
/**
6974
* Create a local Volume instance with no API call.
7075
*
@@ -73,19 +78,22 @@ export class Volume {
7378
* @param token volume auth token.
7479
* @param domain domain for the volume API.
7580
* @param debug whether to use debug mode.
81+
* @param proxy proxy URL for the volume content API.
7682
*/
7783
constructor(
7884
volumeId: string,
7985
name: string,
8086
token: string,
8187
domain?: string,
82-
debug?: boolean
88+
debug?: boolean,
89+
proxy?: string
8390
) {
8491
this.volumeId = volumeId
8592
this.name = name
8693
this.token = token
8794
this.domain = domain
8895
this.debug = debug
96+
this.proxy = proxy
8997
}
9098

9199
/**
@@ -121,7 +129,8 @@ export class Volume {
121129
res.data.name,
122130
res.data.token,
123131
config.domain,
124-
config.debug
132+
config.debug,
133+
config.proxy
125134
)
126135
}
127136

@@ -139,7 +148,14 @@ export class Volume {
139148
): Promise<Volume> {
140149
const config = new ConnectionConfig(opts)
141150
const { name, token } = await Volume.getInfo(volumeId, opts)
142-
return new Volume(volumeId, name, token, config.domain, config.debug)
151+
return new Volume(
152+
volumeId,
153+
name,
154+
token,
155+
config.domain,
156+
config.debug,
157+
config.proxy
158+
)
143159
}
144160

145161
/**

0 commit comments

Comments
 (0)