vercel/ai · error
claude-code bridge did not complete WebSocket handshake with
Error message
claude-code bridge did not complete WebSocket handshake within ${timeoutMs}ms after ${attempt} attempt(s). Last error: ${formatUnknownError(lastError)} What it means
openBridgeWebSocket retries the WebSocket handshake against the claude-code bridge until a deadline (timeoutMs) expires, backing off between attempts. If no attempt succeeds, it throws a plain Error summarizing the timeout, number of attempts, and the last underlying error (formatted via formatUnknownError). This means the bridge server inside the sandbox never completed the WebSocket upgrade in time.
Source
Thrown at packages/harness-claude-code/src/claude-code-harness.ts:1453
attempt++;
try {
const remaining = Math.max(1, deadline - Date.now());
return await openWebSocketAndWaitForBridgeHello({
endpoint,
openTimeoutMs: Math.min(10_000, remaining),
getHelloTimeoutMs: () =>
Math.min(5_000, Math.max(1, deadline - Date.now())),
onHello,
});
} catch (err) {
lastError = err;
const remaining = deadline - Date.now();
if (remaining <= 0) break;
await sleep(Math.min(250 * attempt, 1_000, remaining));
}
}
throw new Error(
`claude-code bridge did not complete WebSocket handshake within ${timeoutMs}ms after ${attempt} attempt(s). Last error: ${formatUnknownError(lastError)}`,
);
}
function webSocketMessageToString(raw: unknown): string {
if (typeof raw === 'string') return raw;
if (Buffer.isBuffer(raw)) return raw.toString('utf8');
if (Array.isArray(raw)) return Buffer.concat(raw).toString('utf8');
if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
if (ArrayBuffer.isView(raw)) {
return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString(
'utf8',
);
}
return String(raw);
}
function withBridgeToken({View on GitHub (pinned to 69428b1f8b)
Solutions
- Read `Last error:` in the message — it contains the root cause (e.g. ECONNREFUSED means the bridge isn't listening; 404 means wrong path).
- Verify the portEndpoint URL points at the actual forwarded bridge port and uses the ws/wss protocol.
- Increase the handshake timeout if the sandbox is slow to boot, or add a readiness wait before connecting.
- Confirm the bridge process is running inside the sandbox and the port is exposed via the sandbox's port forwarding.
Example fix
// before
await connect({ portEndpoint: { url: 'http://127.0.0.1:8080' }, timeoutMs: 5000 });
// after
await connect({ portEndpoint: { url: 'ws://127.0.0.1:8080' }, timeoutMs: 30000 }); Defensive patterns
Strategy: retry
Validate before calling
// pre-flight: ensure the bridge is reachable before the harness connects
const url = new URL(portEndpoint.url);
if (!/^wss?:$/.test(url.protocol)) {
throw new Error(`portEndpoint must be ws(s), got: ${url.protocol}`);
}
await checkTcpReachable(url.hostname, Number(url.port) || 443); Type guard
function isHandshakeTimeoutError(e: unknown): e is Error & { message: string } {
return e instanceof Error && e.message.startsWith('claude-code bridge did not complete WebSocket handshake');
} Try / catch
try {
await connect(...);
} catch (e) {
if (isHandshakeTimeoutError(e)) {
const cause = e.message.split('Last error: ')[1];
logger.error({ cause }, 'bridge handshake failed; check bridge process and portEndpoint URL');
// retry with longer timeout or inspect sandbox logs
}
throw e;
} Prevention
- Use ws:// or wss:// (not http://) in portEndpoint URLs.
- Wait for the bridge process to report readiness before connecting.
- Expose the bridge port in the sandbox config and verify forwarding works.
- Set a generous timeoutMs for cold-started sandboxes and log the embedded lastError.
- Monitor sandbox CPU/startup lag; slow sandboxes routinely exceed tight timeouts.
When it happens
Trigger: Any buildConnect flow where connecting to the bridge WebSocket fails repeatedly (connection refused, TLS errors, bridge process not yet listening, wrong portEndpoint URL) until the retry deadline elapses.
Common situations: Bridge server slow to start in a cold sandbox; incorrect portEndpoint URL/protocol (http vs ws); firewall or proxy blocking the sandbox WebSocket; port not actually forwarded/exposed by the sandbox; bridge crashed on startup — inspect the embedded lastError for the true cause.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Video generation timed out after ${timeoutMs}ms.
- BLACK_FOREST_LABS_VIDEO_GENERATION_TIMEOUT
- Transcription request timed out after 60 seconds
- The claude-code harness needs a TCP port exposed by the sand
- The Claude Code harness requires an explicit `portEndpoint`
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/7770e9ab49c464bf.
Report an issue: GitHub.