router-for-me/CLIProxyAPI · error

set downstream WebRTC answer: %w

Error message

set downstream WebRTC answer: %w

What it means

Thrown when downstream.SetLocalDescription(answer) fails committing the client-facing answer, which also kicks off downstream ICE gathering. Pion fails when the description state is inconsistent (answer already applied, PC closing) — in practice a teardown race or double-call of AcceptUpstreamAnswer.

Source

Thrown at internal/client/codex/live/media.go:444

		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())
	}
	localDescription := s.downstream.LocalDescription()
	if localDescription == nil || strings.TrimSpace(localDescription.SDP) == "" {
		return "", errors.New("downstream WebRTC answer is empty")
	}
	return localDescription.SDP, nil
}

func (s *pionMediaSession) installCandidateTunnels(tunnels []*tcpCandidateTunnel) bool {
	if s == nil {
		return false
	}
	s.tunnelsMu.Lock()

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Ensure AcceptUpstreamAnswer is called at most once per session (wrap with sync.Once or a state flag at the caller)
  2. Retry via a fresh session if the client reconnects
  3. Check PC ConnectionState before applying if adding a precondition
Defensive patterns

Strategy: type-guard

Type guard

func (s *pionMediaSession) canApplyDownstreamAnswer() bool {
	return s != nil && s.downstream != nil &&
		s.downstream.SignalingState() == webrtc.SignalingStateHaveRemoteOffer
}

Try / catch

if errLocal := s.downstream.SetLocalDescription(answer); errLocal != nil {
	var stateErr *webrtc.InvalidStateError
	if errors.As(errLocal, &stateErr) {
		// double-call or teardown race; answer already committed elsewhere
		return "", fmt.Errorf("downstream answer state conflict: %w", errLocal)
	}
	return "", errLocal
}

Prevention

When it happens

Trigger: AcceptUpstreamAnswer invoked twice for one session, or session Close racing the local description commit on the downstream PC.

Common situations: Duplicate answer-delivery retries from the caller, or client disconnect during the final answer step.

Related errors


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