{"record":{"id":"9c6ad5ded1a95824","repo":"can1357/oh-my-pi","slug":"codex-live-signaling-returned-an-empty-sdp-answer","errorCode":null,"errorMessage":"Codex live signaling returned an empty SDP answer","messagePattern":"Codex live signaling returned an empty SDP answer","errorType":"exception","errorClass":"LiveSignalingError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/live/transport.ts","lineNumber":228,"sourceCode":"\t\t});\n\t\tconst fetchImpl = wrapFetchForProxy(fetch, LIVE_PROVIDER);\n\t\tconst response = await fetchImpl(SIGNALING_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders,\n\t\t\tbody: JSON.stringify({\n\t\t\t\tsdp: offer,\n\t\t\t\tsession: buildLiveSessionPayload(this.#options.instructions, this.#options.voice),\n\t\t\t}),\n\t\t\tsignal: this.#options.signal,\n\t\t});\n\t\tconst responseBody = await response.text();\n\t\tif (!response.ok) {\n\t\t\tconst detail = boundedErrorBody(responseBody, response.statusText);\n\t\t\tthrow new LiveSignalingError(response.status, `Codex live signaling failed (${response.status}): ${detail}`);\n\t\t}\n\t\tconst answer = responseBody;\n\t\tif (!answer.trim())\n\t\t\tthrow new LiveSignalingError(response.status, \"Codex live signaling returned an empty SDP answer\");\n\t\tconst callId = parseLiveCallId(response.headers.get(\"location\"));\n\t\tif (!callId) {\n\t\t\tthrow new LiveSignalingError(response.status, \"Codex live signaling returned no valid call ID\");\n\t\t}\n\t\treturn { answer, callId, access, attestation };\n\t}\n\n\tasync #connectSideband(callId: string, access: OAuthAccess, attestation: string | undefined): Promise<void> {\n\t\tlet failure = new Error(\"Codex live sideband connection failed\");\n\t\tfor (let attempt = 0; attempt < SIDEBAND_CONNECT_ATTEMPTS; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait this.#openSideband(callId, access, attestation);\n\t\t\t\treturn;\n\t\t\t} catch (cause) {\n\t\t\t\tfailure = cause instanceof Error ? cause : new Error(String(cause));\n\t\t\t\tif (this.#options.signal?.aborted) throw abortReason(this.#options.signal);\n\t\t\t\tif (attempt + 1 < SIDEBAND_CONNECT_ATTEMPTS) await Bun.sleep(200 * 2 ** attempt);\n\t\t\t}","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/live/transport.ts#L210-L246","documentation":"After a successful signaling HTTP response, #signalWithAccess requires a non-empty SDP answer body. An empty body means the server responded 2xx but did not return the answer SDP needed to complete the WebRTC handshake, so it cannot proceed and throws LiveSignalingError.","triggerScenarios":"The signaling endpoint returns HTTP 200/201 with an empty or whitespace-only body — server-side bug, truncated response through a proxy, or wrong endpoint variant that doesn't produce answers.","commonSituations":"Corporate proxy stripping response bodies, signaling service incident, hitting a health-check or wrong URL instead of the SDP answer endpoint.","solutions":["Retry the signaling call — transient server issues often resolve.","Verify the signaling endpoint URL is the SDP answer endpoint, not another resource.","Check proxy/VPN interference that could truncate or empty response bodies.","Report/inspect the Codex live service status if it persists across retries."],"exampleFix":"// before\nconst { answer } = await transport.signal(offer); // throws on empty body\n// after\nlet result;\nfor (let i = 0; i < 3; i++) {\n  try { result = await transport.signal(offer); break; }\n  catch (e) { if (e instanceof LiveSignalingError && e.message.includes(\"empty SDP\")) await Bun.sleep(2 ** i * 500); else throw e; }\n}","handlingStrategy":"retry","validationCode":"// no pre-call validation is possible; mitigate by retrying empty answers\nconst signalWithRetry = async (offer: string, attempts = 3) => {\n  for (let i = 0; i < attempts; i++) {\n    try { return await transport.signal(offer); }\n    catch (e) {\n      if (e instanceof LiveSignalingError && e.message.includes(\"empty SDP answer\") && i < attempts - 1) { await Bun.sleep(500 * 2 ** i); continue; }\n      throw e;\n    }\n  }\n};","typeGuard":null,"tryCatchPattern":"try {\n  await transport.signal(offer);\n} catch (e) {\n  if (e instanceof LiveSignalingError && e.message.includes(\"empty SDP answer\")) {\n    await retryWithBackoff(() => transport.signal(offer));\n  } else throw e;\n}","preventionTips":["Retry transient signaling failures automatically","Bypass or verify proxies that may strip response bodies","Monitor signaling service health before live sessions"],"tags":["network","webrtc","signaling","codex","empty-response"],"backgroundTag":"empty-response-body","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}