Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/drop-undici7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'e2b': major
---

Drop the dual `undici`/`undici8` dependency: the SDK now depends on `undici@^8` only and requires Node.js >= 22.19.0. undici 8 honors the server's `SETTINGS_MAX_CONCURRENT_STREAMS` per HTTP/2 connection and opens additional connections when a connection's streams are saturated, instead of undici 7's one-request-per-connection multiplexing. On Node.js older than 22.19.0 the SDK no longer loads undici and falls back to the global fetch.

Remove the SDK-level in-flight request cap: `limitConcurrency` and the `E2B_API_INFLIGHT_REQUESTS`, `E2B_ENVD_INFLIGHT_REQUESTS`, and `E2B_ENVD_RPC_INFLIGHT_REQUESTS` env vars are gone. Concurrency is now governed by undici's dispatcher (per-connection HTTP/2 stream limits from the server plus the `E2B_API_CONNECTIONS`/`E2B_ENVD_RPC_CONNECTIONS` connection pools).
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
deno 2.8.1
nodejs 22.18.0
nodejs 22.19.0
pnpm 10.34.5
python 3.10
uv 0.10.0
2 changes: 1 addition & 1 deletion codegen.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ENV PATH="/go/bin:${PATH}"
RUN pip install black==26.3.1 e2b-openapi-python-client==0.26.2 datamodel-code-generator==0.64.0 protoc-gen-connectrpc==0.11.1 protoc-gen-py==0.1.1

# Install Node.js (pinned to match .tool-versions)
ENV NODE_VERSION=22.18.0
ENV NODE_VERSION=22.19.0
RUN ARCH=$(uname -m) && \
case "$ARCH" in \
x86_64) NODE_ARCH="x64" ;; \
Expand Down
7 changes: 2 additions & 5 deletions packages/js-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,10 @@
"openapi-fetch": "^0.14.1",
"platform": "^1.3.6",
"tar": "^7.5.19",
"undici": "^7.29.0"
},
"optionalDependencies": {
"undici8": "npm:undici@8.10.0"
"undici": "^8.10.0"
},
"engines": {
"node": ">=20.18.1 <21 || >=22"
"node": ">=22.19.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Align the CLI's Node floor with the SDK

When the changesets release process updates @e2b/cli's workspace:^ dependency to this new SDK major, the CLI will still advertise Node >=20.18.1 <21 || >=22.9.0, while this dependency rejects every version below 22.19.0. Consequently, installing the current CLI on its documented Node 20 or Node 22.9–22.18 range fails for users with strict engine enforcement (including this repository's pnpm configuration), before the intended global-fetch fallback can run. Update the CLI engine range and changeset/tests as part of this breaking release, or retain a compatible SDK engine range for the fallback path.

Useful? React with 👍 / 👎.

},
"browserslist": [
"defaults"
Expand Down
21 changes: 1 addition & 20 deletions packages/js-sdk/src/api/http2.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { runtime } from '../utils'
import { parseInflightLimitEnv, parsePositiveIntEnv } from './metadata'
import { parsePositiveIntEnv } from './metadata'
import {
buildDispatchedFetch,
createRuntimeFetch,
type UndiciModule,
} from '../undici'

const DEFAULT_API_CONNECTION_LIMIT = 100
// 1000 = ~10 streams per connection (with the 100-conn default).
// Override via env if your workload needs different.
const DEFAULT_API_INFLIGHT_LIMIT = 1000

// Fetchers are cached per proxy so requests without a proxy keep sharing a
// single dispatcher while each distinct proxy URL gets its own.
Expand All @@ -33,7 +30,6 @@ export function createApiFetchForRuntime(
currentRuntime = runtime,
options: {
connectionLimit?: number
inflightLimit?: number
proxy?: string
loadUndici?: () => Promise<UndiciModule | undefined>
} = {}
Expand All @@ -43,7 +39,6 @@ export function createApiFetchForRuntime(
return createRuntimeFetch(currentRuntime, () =>
buildDispatchedFetch({
connections: options.connectionLimit ?? getApiConnectionLimit(),
inflightLimit: options.inflightLimit ?? getApiInflightLimit(),
proxy: options.proxy,
loadUndici: options.loadUndici,
})
Expand All @@ -56,17 +51,3 @@ export function getApiConnectionLimit(): number {
DEFAULT_API_CONNECTION_LIMIT
)
}

/**
* Returns the configured max number of API requests that can be in flight at
* once, or `0` to disable the cap.
*
* Defaults to `1000` ({@link DEFAULT_API_INFLIGHT_LIMIT}). Override via
* `E2B_API_INFLIGHT_REQUESTS` env var; set to `0` to disable the cap entirely.
*/
export function getApiInflightLimit(): number {
return parseInflightLimitEnv(
'E2B_API_INFLIGHT_REQUESTS',
DEFAULT_API_INFLIGHT_LIMIT
)
}
82 changes: 0 additions & 82 deletions packages/js-sdk/src/api/inflight.ts

This file was deleted.

21 changes: 0 additions & 21 deletions packages/js-sdk/src/api/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,24 +62,3 @@ export function parsePositiveIntEnv(

return parsed
}

/**
* Parse an inflight-limit env var. Returns `0` to disable the cap (documented
* opt-out) or a positive integer to cap concurrency. Throws on non-integer or
* negative values so misconfiguration is surfaced loudly rather than silently
* removing the cap. A return value of `0` is recognized by
* {@link limitConcurrency} as "no cap".
*/
export function parseInflightLimitEnv(
name: string,
defaultValue: number
): number {
const parsed = parseIntEnv(name, defaultValue)
if (parsed < 0) {
throw new Error(
`Invalid ${name}=${parsed}: expected a non-negative integer ` +
'(use 0 to disable the cap).'
)
}
return parsed
}
40 changes: 1 addition & 39 deletions packages/js-sdk/src/envd/http2.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { runtime } from '../utils'
import { parseInflightLimitEnv, parsePositiveIntEnv } from '../api/metadata'
import { parsePositiveIntEnv } from '../api/metadata'
import {
buildDispatchedFetch,
createRuntimeFetch,
Expand All @@ -8,7 +8,6 @@ import {

type EnvdFetchOptions = {
connectionLimit?: number
inflightLimit?: number
proxy?: string
loadUndici?: () => Promise<UndiciModule | undefined>
}
Expand All @@ -19,8 +18,6 @@ const envdFetchers = new Map<string, typeof fetch>()
const envdRpcFetchers = new Map<string, typeof fetch>()
const DEFAULT_ENVD_CONNECTION_LIMIT = 10
const DEFAULT_ENVD_RPC_CONNECTION_LIMIT = 200
const DEFAULT_ENVD_INFLIGHT_LIMIT = 2000
const DEFAULT_ENVD_RPC_INFLIGHT_LIMIT = 2000

export function createEnvdFetchForRuntime(
currentRuntime = runtime,
Expand All @@ -29,7 +26,6 @@ export function createEnvdFetchForRuntime(
return createRuntimeFetch(currentRuntime, () =>
buildDispatchedFetch({
connections: options.connectionLimit ?? DEFAULT_ENVD_CONNECTION_LIMIT,
inflightLimit: options.inflightLimit ?? 0,
proxy: options.proxy,
loadUndici: options.loadUndici,
})
Expand All @@ -47,7 +43,6 @@ export function createEnvdFetch(proxy?: string): typeof fetch {
// Keep one origin connection for short envd REST calls. If ALPN falls back
// to h1, this favors connection pressure over per-sandbox throughput.
const envdFetch = createEnvdFetchForRuntime(runtime, {
inflightLimit: getEnvdInflightLimit(),
proxy,
})
envdFetchers.set(key, envdFetch)
Expand All @@ -65,7 +60,6 @@ export function createEnvdRpcFetch(proxy?: string): typeof fetch {

const envdRpcFetch = createEnvdFetchForRuntime(runtime, {
connectionLimit: getEnvdRpcConnectionLimit(),
inflightLimit: getEnvdRpcInflightLimit(),
proxy,
})
envdRpcFetchers.set(key, envdRpcFetch)
Expand All @@ -79,35 +73,3 @@ export function getEnvdRpcConnectionLimit(): number {
DEFAULT_ENVD_RPC_CONNECTION_LIMIT
)
}

/**
* Returns the configured max number of envd REST requests (e.g.
* `files.read`/`files.write`) that can be in flight at once across all
* sandboxes in this SDK process, or `0` to disable the cap.
*
* Defaults to `2000` ({@link DEFAULT_ENVD_INFLIGHT_LIMIT}). Override
* via `E2B_ENVD_INFLIGHT_REQUESTS` env var; set to `0` to disable the cap
* entirely.
*/
export function getEnvdInflightLimit(): number {
return parseInflightLimitEnv(
'E2B_ENVD_INFLIGHT_REQUESTS',
DEFAULT_ENVD_INFLIGHT_LIMIT
)
}

/**
* Returns the configured max number of envd RPC requests that
* can be in flight at once across all sandboxes in this SDK process,
* or `0` to disable the cap.
*
* Defaults to `2000` ({@link DEFAULT_ENVD_RPC_INFLIGHT_LIMIT}). Override
* via `E2B_ENVD_RPC_INFLIGHT_REQUESTS` env var; set to `0` to disable the cap
* entirely.
*/
export function getEnvdRpcInflightLimit(): number {
return parseInflightLimitEnv(
'E2B_ENVD_RPC_INFLIGHT_REQUESTS',
DEFAULT_ENVD_RPC_INFLIGHT_LIMIT
)
}
23 changes: 9 additions & 14 deletions packages/js-sdk/src/undici.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { compareVersions } from 'compare-versions'

import { limitConcurrency } from './api/inflight'
import { isReadableStreamLike, isRequestLike } from './is'
import { dynamicImport, toDispatchableStream } from './utils'

Expand All @@ -20,22 +19,22 @@ export type UndiciModule = {
fetch: unknown
}

const UNDICI_8_MIN_NODE = '22.19.0'
const UNDICI_MIN_NODE = '22.19.0'

export function getUndiciPackageCandidates(nodeVersion: string): string[] {
if (compareVersions(nodeVersion, UNDICI_8_MIN_NODE) >= 0) {
return ['undici8', 'undici']
if (compareVersions(nodeVersion, UNDICI_MIN_NODE) >= 0) {
return ['undici']
}

return ['undici']
return []
Comment on lines +25 to +29

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not a TASTE rule, a behavior regression the diff makes easy to miss. buildApi.ts documents that undici 8 doesn't import on Bun, which is exactly why undici (7) was the trailing candidate. With undici 7 gone and the gate returning [] below 22.19.0, loadUndici() resolves to undefined on any runtime whose process.versions.node is below the gate (Bun's emulated version) — so uploadFile loses the undici fetch it prefers for honoring an explicit Content-Length on stream bodies. Worth confirming against test:bun before merging, and either way the changeset should mention Bun, since it only mentions Node.

Also, now that the list can hold at most one entry, the plural ...Candidates name and the loop in loadUndici are vestigial — a supportsUndici(nodeVersion): boolean (or returning 'undici' | undefined) says what is actually being decided.

}

export async function loadUndici(): Promise<UndiciModule | undefined> {
for (const packageName of getUndiciPackageCandidates(process.versions.node)) {
try {
return await dynamicImport<UndiciModule>(packageName)
} catch {
// Try the next package supported by this Node version.
// Fall back to the global fetch when undici cannot load.
}
}

Expand Down Expand Up @@ -88,20 +87,18 @@ export function createRuntimeFetch(

/**
* Build a fetch bound to a bounded undici dispatcher (HTTP/2 enabled,
* `connections` origin connections, optional proxy tunnel), capped at
* `inflightLimit` in-flight requests (`0` disables the cap). Falls back to
* the global fetch — still capped — when undici cannot be loaded.
* `connections` origin connections, optional proxy tunnel). Falls back to
* the global fetch when undici cannot be loaded.
*/
export async function buildDispatchedFetch(options: {
connections: number
inflightLimit: number
proxy?: string
loadUndici?: () => Promise<UndiciModule | undefined>
}): Promise<typeof fetch> {
const undici = await (options.loadUndici ?? loadUndici)()

if (!undici) {
return limitConcurrency(lateBoundGlobalFetch(), options.inflightLimit)
return lateBoundGlobalFetch()
}

const { Agent, ProxyAgent, fetch: undiciFetch } = undici
Expand All @@ -121,16 +118,14 @@ export async function buildDispatchedFetch(options: {
init?: UndiciRequestInit
) => Promise<Response>

const wrapped: typeof fetch = ((input, init) => {
return ((input, init) => {
const request = toUndiciRequestInput(input, init)

return fetchWithDispatcher(request.input, {
...request.init,
dispatcher,
})
}) as typeof fetch

return limitConcurrency(wrapped, options.inflightLimit)
}

function toUndiciRequestInput(
Expand Down
25 changes: 0 additions & 25 deletions packages/js-sdk/tests/api/http2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ afterEach(() => {
vi.doUnmock('undici')
vi.doUnmock('../../src/utils')
delete process.env.E2B_API_CONNECTIONS
delete process.env.E2B_API_INFLIGHT_REQUESTS
})

test('uses undici with a bounded HTTP/2 dispatcher for API requests', async () => {
Expand Down Expand Up @@ -166,27 +165,3 @@ test('getApiConnectionLimit throws on a malformed env value', async () => {

expect(() => getApiConnectionLimit()).toThrow(/E2B_API_CONNECTIONS/)
})

test('getApiInflightLimit throws on a malformed env value', async () => {
process.env.E2B_API_INFLIGHT_REQUESTS = 'not-a-number'

const { getApiInflightLimit } = await import('../../src/api/http2')

expect(() => getApiInflightLimit()).toThrow(/E2B_API_INFLIGHT_REQUESTS/)
})

test('getApiInflightLimit returns 0 when explicitly disabled', async () => {
process.env.E2B_API_INFLIGHT_REQUESTS = '0'

const { getApiInflightLimit } = await import('../../src/api/http2')

expect(getApiInflightLimit()).toBe(0)
})

test('getApiInflightLimit throws on negative env value', async () => {
process.env.E2B_API_INFLIGHT_REQUESTS = '-5'

const { getApiInflightLimit } = await import('../../src/api/http2')

expect(() => getApiInflightLimit()).toThrow(/E2B_API_INFLIGHT_REQUESTS=-5/)
})
Loading
Loading