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
- Ensure AcceptUpstreamAnswer is called at most once per session (wrap with sync.Once or a state flag at the caller)
- Retry via a fresh session if the client reconnects
- 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
- Call AcceptUpstreamAnswer exactly once per session (guard with sync.Once at the caller)
- Check SignalingState before committing if adding preconditions
- Route duplicate answer deliveries to a no-op instead of an error path
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
- set upstream WebRTC offer: %w
- create downstream WebRTC answer: %w
- create upstream WebRTC offer: %w
- upstream WebRTC offer is empty
- downstream WebRTC answer is empty
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/7bf117b61113201c.
Report an issue: GitHub.