{"record":{"id":"7770e9ab49c464bf","repo":"vercel/ai","slug":"claude-code-bridge-did-not-complete-websocket-hand","errorCode":null,"errorMessage":"claude-code bridge did not complete WebSocket handshake within ${timeoutMs}ms after ${attempt} attempt(s). Last error: ${formatUnknownError(lastError)}","messagePattern":"claude-code bridge did not complete WebSocket handshake within (.+?)ms after (.+?) attempt\\(s\\)\\. Last error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/harness-claude-code/src/claude-code-harness.ts","lineNumber":1453,"sourceCode":"    attempt++;\n    try {\n      const remaining = Math.max(1, deadline - Date.now());\n      return await openWebSocketAndWaitForBridgeHello({\n        endpoint,\n        openTimeoutMs: Math.min(10_000, remaining),\n        getHelloTimeoutMs: () =>\n          Math.min(5_000, Math.max(1, deadline - Date.now())),\n        onHello,\n      });\n    } catch (err) {\n      lastError = err;\n      const remaining = deadline - Date.now();\n      if (remaining <= 0) break;\n      await sleep(Math.min(250 * attempt, 1_000, remaining));\n    }\n  }\n\n  throw new Error(\n    `claude-code bridge did not complete WebSocket handshake within ${timeoutMs}ms after ${attempt} attempt(s). Last error: ${formatUnknownError(lastError)}`,\n  );\n}\n\nfunction webSocketMessageToString(raw: unknown): string {\n  if (typeof raw === 'string') return raw;\n  if (Buffer.isBuffer(raw)) return raw.toString('utf8');\n  if (Array.isArray(raw)) return Buffer.concat(raw).toString('utf8');\n  if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');\n  if (ArrayBuffer.isView(raw)) {\n    return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString(\n      'utf8',\n    );\n  }\n  return String(raw);\n}\n\nfunction withBridgeToken({","sourceCodeStart":1435,"sourceCodeEnd":1471,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/harness-claude-code/src/claude-code-harness.ts#L1435-L1471","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait connect({ portEndpoint: { url: 'http://127.0.0.1:8080' }, timeoutMs: 5000 });\n// after\nawait connect({ portEndpoint: { url: 'ws://127.0.0.1:8080' }, timeoutMs: 30000 });","handlingStrategy":"retry","validationCode":"// pre-flight: ensure the bridge is reachable before the harness connects\nconst url = new URL(portEndpoint.url);\nif (!/^wss?:$/.test(url.protocol)) {\n  throw new Error(`portEndpoint must be ws(s), got: ${url.protocol}`);\n}\nawait checkTcpReachable(url.hostname, Number(url.port) || 443);","typeGuard":"function isHandshakeTimeoutError(e: unknown): e is Error & { message: string } {\n  return e instanceof Error && e.message.startsWith('claude-code bridge did not complete WebSocket handshake');\n}","tryCatchPattern":"try {\n  await connect(...);\n} catch (e) {\n  if (isHandshakeTimeoutError(e)) {\n    const cause = e.message.split('Last error: ')[1];\n    logger.error({ cause }, 'bridge handshake failed; check bridge process and portEndpoint URL');\n    // retry with longer timeout or inspect sandbox logs\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["network","websocket","timeout","sandbox"],"backgroundTag":"websocket-handshake-timeout","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}