router-for-me/CLIProxyAPI · error
gather downstream WebRTC candidates: %w
Error message
gather downstream WebRTC candidates: %w
What it means
Thrown when the context is cancelled while waiting for downstream ICE candidate gathering to finish (select on gatherComplete vs ctx.Done()), wrapping context.Canceled/DeadlineExceeded. The downstream answer has been committed but full candidate gathering did not complete before the caller gave up.
Source
Thrown at internal/client/codex/live/media.go:449
}); 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()
defer s.tunnelsMu.Unlock()
select {
case <-s.done:
return false
default:View on GitHub (pinned to 78f0c4079e)
Solutions
- Raise the context deadline for the answer call (gathering normally completes quickly)
- Verify relay network settings: PublicIP correctness, UDP port range open in the firewall
- Do not cancel the context while the client still awaits the answer
- If gathering legitimately stalls, consider disabling mDNS candidate gathering in the SettingEngine for server deployments
Example fix
// before ctx, cancel := context.WithTimeout(ctx, 1*time.Second) answer, err := session.AcceptUpstreamAnswer(ctx, upstreamAnswerSDP) // after ctx, cancel := context.WithTimeout(ctx, 15*time.Second) answer, err := session.AcceptUpstreamAnswer(ctx, upstreamAnswerSDP)
Defensive patterns
Strategy: validation
Validate before calling
if err := ctx.Err(); err != nil {
return "", err // skip answer work entirely on cancelled context
} Try / catch
select {
case <-gatherComplete:
case <-ctx.Done():
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
log.Warn("downstream ICE gathering deadline exceeded; check relay PublicIP/UDP firewall")
}
return "", fmt.Errorf("gather downstream WebRTC candidates: %w", ctx.Err())
} Prevention
- Give the answer phase a deadline >=10s; gathering is normally sub-second
- Open the configured UDP port range in the host firewall so host candidates gather promptly
- Verify PublicIP points to a real routable address when NAT 1:1 is configured
- Keep the client connected until the answer is returned
When it happens
Trigger: Client disconnects or the request deadline expires during the final answer phase; gathering stalls when no usable network candidates exist on the downstream API (e.g. misconfigured PublicIP/NAT 1:1 settings or filtered interfaces).
Common situations: Short HTTP timeouts wrapping the answer endpoint; containers with restricted UDP where downstream host candidates gather slowly; PublicIP set to an unreachable address slowing gathering.
Related errors
- gather upstream WebRTC candidates: %w
- SDP contains incomplete ICE credentials
- SDP contains inconsistent bundled ICE credentials
- create downstream PeerConnection: %w
- create upstream PeerConnection: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/51b6dd42206c5176.
Report an issue: GitHub.