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

  1. If not using a proxy route, verify the raw upstream answer is passed unmodified
  2. If proxied, check that the proxy is a plain CONNECT-style forwarder that does not mangle payloads
  3. Discard late answers: do not call AcceptUpstreamAnswer after Close or after a retry started a new session
  4. 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

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


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/b4beda8357df8470. Report an issue: GitHub.