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

  1. Raise the context deadline for the answer call (gathering normally completes quickly)
  2. Verify relay network settings: PublicIP correctness, UDP port range open in the firewall
  3. Do not cancel the context while the client still awaits the answer
  4. 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

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


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