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

  1. Increase or remove the deadline on the context passed to NewSession/AcceptUpstreamAnswer
  2. Keep the client connected during negotiation (gathering usually completes in <1s with host candidates)
  3. For proxied deployments verify loopback networking works inside the container (proxyUpstreamAPI is loopback-only)
  4. 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

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


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