can1357/oh-my-pi · error · LiveSignalingError

Codex live signaling returned an empty SDP answer

Error message

Codex live signaling returned an empty SDP answer

What it means

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.

Source

Thrown at packages/coding-agent/src/live/transport.ts:228

		});
		const fetchImpl = wrapFetchForProxy(fetch, LIVE_PROVIDER);
		const response = await fetchImpl(SIGNALING_URL, {
			method: "POST",
			headers,
			body: JSON.stringify({
				sdp: offer,
				session: buildLiveSessionPayload(this.#options.instructions, this.#options.voice),
			}),
			signal: this.#options.signal,
		});
		const responseBody = await response.text();
		if (!response.ok) {
			const detail = boundedErrorBody(responseBody, response.statusText);
			throw new LiveSignalingError(response.status, `Codex live signaling failed (${response.status}): ${detail}`);
		}
		const answer = responseBody;
		if (!answer.trim())
			throw new LiveSignalingError(response.status, "Codex live signaling returned an empty SDP answer");
		const callId = parseLiveCallId(response.headers.get("location"));
		if (!callId) {
			throw new LiveSignalingError(response.status, "Codex live signaling returned no valid call ID");
		}
		return { answer, callId, access, attestation };
	}

	async #connectSideband(callId: string, access: OAuthAccess, attestation: string | undefined): Promise<void> {
		let failure = new Error("Codex live sideband connection failed");
		for (let attempt = 0; attempt < SIDEBAND_CONNECT_ATTEMPTS; attempt++) {
			try {
				await this.#openSideband(callId, access, attestation);
				return;
			} catch (cause) {
				failure = cause instanceof Error ? cause : new Error(String(cause));
				if (this.#options.signal?.aborted) throw abortReason(this.#options.signal);
				if (attempt + 1 < SIDEBAND_CONNECT_ATTEMPTS) await Bun.sleep(200 * 2 ** attempt);
			}

View on GitHub (pinned to 9690622007)

Solutions

  1. Retry the signaling call — transient server issues often resolve.
  2. Verify the signaling endpoint URL is the SDP answer endpoint, not another resource.
  3. Check proxy/VPN interference that could truncate or empty response bodies.
  4. Report/inspect the Codex live service status if it persists across retries.

Example fix

// before
const { answer } = await transport.signal(offer); // throws on empty body
// after
let result;
for (let i = 0; i < 3; i++) {
  try { result = await transport.signal(offer); break; }
  catch (e) { if (e instanceof LiveSignalingError && e.message.includes("empty SDP")) await Bun.sleep(2 ** i * 500); else throw e; }
}
Defensive patterns

Strategy: retry

Validate before calling

// no pre-call validation is possible; mitigate by retrying empty answers
const signalWithRetry = async (offer: string, attempts = 3) => {
  for (let i = 0; i < attempts; i++) {
    try { return await transport.signal(offer); }
    catch (e) {
      if (e instanceof LiveSignalingError && e.message.includes("empty SDP answer") && i < attempts - 1) { await Bun.sleep(500 * 2 ** i); continue; }
      throw e;
    }
  }
};

Try / catch

try {
  await transport.signal(offer);
} catch (e) {
  if (e instanceof LiveSignalingError && e.message.includes("empty SDP answer")) {
    await retryWithBackoff(() => transport.signal(offer));
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: Corporate proxy stripping response bodies, signaling service incident, hitting a health-check or wrong URL instead of the SDP answer endpoint.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/9c6ad5ded1a95824. Report an issue: GitHub.