router-for-me/CLIProxyAPI · error
gather upstream WebRTC candidates: %w
Error message
gather upstream WebRTC candidates: %w
What it means
Thrown when the context is cancelled while waiting for ICE candidate gathering to complete on the upstream PC (select on gatherComplete vs ctx.Done()). It wraps the context error (context.Canceled or context.DeadlineExceeded), meaning the caller abandoned the session before a full set of upstream candidates was collected.
Source
Thrown at internal/client/codex/live/media.go:395
return nil, "", fmt.Errorf("create upstream DataChannel: %w", errChannel)
}
session.bridge.attachUpstream(upstreamChannel)
gatherComplete := webrtc.GatheringCompletePromise(upstream)
offer, errOffer := upstream.CreateOffer(nil)
if errOffer != nil {
_ = session.Close()
return nil, "", fmt.Errorf("create upstream WebRTC offer: %w", errOffer)
}
if errLocal := upstream.SetLocalDescription(offer); errLocal != nil {
_ = session.Close()
return nil, "", fmt.Errorf("set upstream WebRTC offer: %w", errLocal)
}
select {
case <-gatherComplete:
case <-ctx.Done():
_ = session.Close()
return nil, "", fmt.Errorf("gather upstream WebRTC candidates: %w", ctx.Err())
}
localDescription := upstream.LocalDescription()
if localDescription == nil || strings.TrimSpace(localDescription.SDP) == "" {
_ = session.Close()
return nil, "", errors.New("upstream WebRTC offer is empty")
}
session.localOffer = localDescription.SDP
return session, localDescription.SDP, nil
}
func (s *pionMediaSession) AcceptUpstreamAnswer(ctx context.Context, upstreamAnswer string) (string, error) {
if s == nil || s.upstream == nil || s.downstream == nil {
return "", errors.New("Codex live media session unavailable")
}
answerToApply := upstreamAnswer
if s.proxyDialer != nil {
rewrittenAnswer, tunnels, errProxy := prepareProxiedUpstreamAnswer(upstreamAnswer, s.localOffer, s.proxyDialer)
if errProxy != nil {View on GitHub (pinned to 78f0c4079e)
Solutions
- Increase or remove the deadline on the context passed to NewSession/AcceptUpstreamAnswer
- Keep the client connected during negotiation (gathering usually completes in <1s with host candidates)
- For proxied deployments verify loopback networking works inside the container (proxyUpstreamAPI is loopback-only)
- Check relayConfig.PublicIP / port range so host candidates are generatable
Example fix
// before ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) // after ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
Defensive patterns
Strategy: validation
Validate before calling
if err := ctx.Err(); err != nil {
return nil, "", err // do not start gathering with an already-cancelled context
} Try / catch
select {
case <-gatherComplete:
case <-ctx.Done():
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
log.Warn("ICE gathering exceeded deadline; consider raising context timeout")
}
return nil, "", fmt.Errorf("gather upstream WebRTC candidates: %w", ctx.Err())
} Prevention
- Pass a context with generous deadline (>=10s) to NewSession/AcceptUpstreamAnswer
- Verify container networking (loopback, UDP) so gathering cannot stall
- Cancel only when the client is truly gone — cancellation here wastes a fully set-up session
When it happens
Trigger: HTTP request context cancelled (client disconnected), a short deadline on the session-establishment context, or gathering stalled because no network interfaces/candidates are available (e.g. fully firewalled host with no loopback route on the proxy API).
Common situations: Client cancelling the WebRTC offer request mid-negotiation; aggressive per-request timeouts in front of the media endpoint; a container with restricted networking where ICE gathering hangs.
Related errors
- gather downstream 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/c751ee9ed9f52152.
Report an issue: GitHub.