router-for-me/CLIProxyAPI · error
set upstream WebRTC answer: %w
Error message
set upstream WebRTC answer: %w
What it means
Thrown when upstream.SetRemoteDescription fails applying OpenAI's answer SDP on the upstream PeerConnection. Pion rejects answers whose SDP does not match the previously set local offer (different media sections, ICE ufrag/pwd mismatch, missing attributes), malformed SDP, or an invalid state for an answer. On the proxied path the answer was rewritten by prepareProxiedUpstreamAnswer first — a bad rewrite produces the same failure.
Source
Thrown at internal/client/codex/live/media.go:432
return "", errProxy
}
for _, tunnel := range tunnels {
tunnel.setForwardingStartedHandler(s.logForwardingStarted)
}
if !s.installCandidateTunnels(tunnels) {
errClosed := errors.New("Codex live media session closed while configuring TCP proxy")
if errClose := closeCandidateTunnels(tunnels); errClose != nil {
return "", errors.Join(errClosed, fmt.Errorf("close TCP candidate tunnels: %w", errClose))
}
return "", errClosed
}
answerToApply = rewrittenAnswer
}
if errRemote := s.upstream.SetRemoteDescription(webrtc.SessionDescription{
Type: webrtc.SDPTypeAnswer,
SDP: answerToApply,
}); errRemote != nil {
errSetRemote := fmt.Errorf("set upstream WebRTC answer: %w", errRemote)
if errClose := s.closeCandidateTunnels(); errClose != nil {
return "", errors.Join(errSetRemote, fmt.Errorf("close TCP candidate tunnels: %w", errClose))
}
return "", errSetRemote
}
gatherComplete := webrtc.GatheringCompletePromise(s.downstream)
answer, errAnswer := s.downstream.CreateAnswer(nil)
if errAnswer != nil {
return "", fmt.Errorf("create downstream WebRTC answer: %w", errAnswer)
}
if errLocal := s.downstream.SetLocalDescription(answer); errLocal != nil {
return "", fmt.Errorf("set downstream WebRTC answer: %w", errLocal)
}
select {
case <-gatherComplete:
case <-ctx.Done():
return "", fmt.Errorf("gather downstream WebRTC candidates: %w", ctx.Err())
}View on GitHub (pinned to 78f0c4079e)
Solutions
- If not using a proxy route, verify the raw upstream answer is passed unmodified
- If proxied, check that the proxy is a plain CONNECT-style forwarder that does not mangle payloads
- Discard late answers: do not call AcceptUpstreamAnswer after Close or after a retry started a new session
- Compare localOffer m-lines with the answer's to spot mismatch (audio + application sections expected)
Defensive patterns
Strategy: try-catch
Validate before calling
func isLikelyAnswerMismatch(err error) bool {
return err != nil && strings.Contains(err.Error(), "set upstream WebRTC answer")
} Try / catch
if _, err := session.AcceptUpstreamAnswer(ctx, answer); err != nil {
var stateErr *webrtc.InvalidStateError
switch {
case errors.As(err, &stateErr):
log.WithError(err).Warn("answer applied to closed/dirty upstream PC; discard session")
default:
log.WithError(err).Error("upstream answer rejected; check SDP/proxy rewriting")
}
} Prevention
- Deliver the upstream answer exactly once and only to the session that issued the offer
- Discard answers arriving after retries started a new session
- On proxied routes, verify the proxy does not alter SDP payloads
When it happens
Trigger: The answer SDP from upstream is corrupted by the TCP-proxy candidate rewriting, the answer does not correspond to localOffer, or the upstream PC is already closed when the answer arrives.
Common situations: Proxy-mode candidate rewriting bugs (version-sensitive), a late answer arriving after session teardown/retry, or upstream protocol changes in the Codex live handshake.
Related errors
- set downstream WebRTC offer: %w
- create downstream WebRTC answer: %w
- upstream WebRTC offer is empty
- downstream WebRTC answer is empty
- upstream WebRTC answer has no supported public TCP passive c
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/b4beda8357df8470.
Report an issue: GitHub.