Summary
Every runCommand({ wait: true }) call (i.e. the default) throws Expected a stream of command data when the SDK runs on Node ≥26. The Vercel API responds correctly — the SDK just can't read the response because of an undici version mismatch between the SDK's bundled undici@7.x (used to build the dispatcher Agent) and Node 26's built-in undici@8.x (used by globalThis.fetch).
Versions
@vercel/sandbox: tested with 1.8.0 and 1.10.2 (both fail)
- Node: 26.1.0
- SDK's bundled undici:
7.22.0 (node_modules/@vercel/sandbox/dist/api-client/base-client.js imports Agent from this)
- Node 26's built-in undici (powering
globalThis.fetch): 8.3.0
Repro
import { Sandbox } from '@vercel/sandbox';
const sb = await Sandbox.create({
runtime: 'node24',
token: process.env.VERCEL_TOKEN!,
projectId: process.env.VERCEL_PROJECT_ID!,
teamId: process.env.VERCEL_TEAM_ID!,
});
await sb.runCommand({ cmd: 'echo', args: ['hello'], sudo: true });
// → APIError: Expected a stream of command data
Stack:
APIClient.runCommand node_modules/@vercel/sandbox/src/api-client/api-client.ts:249:15
DisposableSandbox._runCommand node_modules/@vercel/sandbox/src/sandbox.ts:450:29
Root cause
base-client.js builds the dispatcher with the SDK's bundled undici@7:
import { Agent } from "undici";
const DEFAULT_AGENT = new Agent({ bodyTimeout: 0 });
...
const response = await this.fetch(url.toString(), {
...opts,
dispatcher: this.agent, // ← undici@7 Agent
...
});
Node 26's globalThis.fetch (undici@8) accepts the dispatcher, but the response object it returns is broken: response headers do not surface and content-encoding: br bodies are not decoded.
api-client.js:249 then throws because:
if (response.headers.get("content-type") !== "application/x-ndjson") {
throw new APIError(response, { message: "Expected a stream of command data" });
}
…even though the server's Content-Type is application/x-ndjson.
Diagnostic evidence
Calling the same API endpoint via globalThis.fetch directly (no dispatcher) — same URL, headers, body:
Status: 200
Content-Type: application/x-ndjson
Content-Encoding: br
Body length: 473
Body: {\"command\":{...,\"exitCode\":null}}
{\"command\":{...,\"exitCode\":0}}
Calling via the SDK's client.request(...) (with the undici@7 Agent dispatcher):
Status: 200 OK
Content-Type: null
Content-Encoding: null
All header keys: (empty)
Body length: 195
Body (raw): <binary brotli garbage>
Same request, different response object — that's the dispatcher mismatch.
Workaround for users
Set the SDK client's Agent to undefined immediately after Sandbox.create() so fetch uses Node's default pool:
const sb = await Sandbox.create({...});
const client = await (sb as any).ensureClient();
client.agent = undefined;
// runCommand now works
Confirmed end-to-end (dnf install exits 0).
Suggested fix
Bump the SDK's bundled undici to v8 so the Agent matches Node 26's built-in fetch, or stop passing dispatcher and let fetch use the default pool, or add a runtime check that disables the custom dispatcher when process.versions.undici differs from the bundled version.
Impact
Anyone running the SDK on Node 26 (current LTS line) hits this on the first runCommand — i.e. the SDK is effectively unusable on Node 26 without the workaround.
Summary
Every
runCommand({ wait: true })call (i.e. the default) throwsExpected a stream of command datawhen the SDK runs on Node ≥26. The Vercel API responds correctly — the SDK just can't read the response because of an undici version mismatch between the SDK's bundledundici@7.x(used to build thedispatcherAgent) and Node 26's built-inundici@8.x(used byglobalThis.fetch).Versions
@vercel/sandbox: tested with 1.8.0 and 1.10.2 (both fail)7.22.0(node_modules/@vercel/sandbox/dist/api-client/base-client.jsimportsAgentfrom this)globalThis.fetch):8.3.0Repro
Stack:
Root cause
base-client.jsbuilds the dispatcher with the SDK's bundled undici@7:Node 26's
globalThis.fetch(undici@8) accepts the dispatcher, but the response object it returns is broken: response headers do not surface andcontent-encoding: brbodies are not decoded.api-client.js:249then throws because:…even though the server's
Content-Typeisapplication/x-ndjson.Diagnostic evidence
Calling the same API endpoint via
globalThis.fetchdirectly (no dispatcher) — same URL, headers, body:Calling via the SDK's
client.request(...)(with the undici@7 Agent dispatcher):Same request, different response object — that's the dispatcher mismatch.
Workaround for users
Set the SDK client's Agent to
undefinedimmediately afterSandbox.create()so fetch uses Node's default pool:Confirmed end-to-end (
dnf installexits 0).Suggested fix
Bump the SDK's bundled
undicito v8 so the Agent matches Node 26's built-in fetch, or stop passingdispatcherand let fetch use the default pool, or add a runtime check that disables the custom dispatcher whenprocess.versions.undicidiffers from the bundled version.Impact
Anyone running the SDK on Node 26 (current LTS line) hits this on the first
runCommand— i.e. the SDK is effectively unusable on Node 26 without the workaround.