router-for-me/CLIProxyAPI · error

downstream WebRTC answer is empty

Error message

downstream WebRTC answer is empty

What it means

Thrown when creating the downstream WebRTC answer: after SetLocalDescription and ICE gathering completed on the downstream peer connection, LocalDescription() returned nil or an empty SDP. The downstream half of the relay therefore has no answer to return to the connecting client. Like the upstream variant, it signals an internal pion state failure rather than a client mistake.

Source

Thrown at internal/client/codex/live/media.go:453

		}
		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:
	}
	s.tunnels = tunnels
	return true
}

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Check logs for the preceding 'create downstream WebRTC answer' / 'set downstream WebRTC answer' / 'gather downstream WebRTC candidates' errors which usually carry the real cause
  2. Verify codex.live-media-relay UDP port range and public-ip settings so the relay can bind and advertise candidates
  3. Confirm nothing closed the downstream peer connection concurrently (session teardown, context cancel)
  4. Upgrade/pin the pion/webrtc dependency if the state is consistently reproducible
Defensive patterns

Strategy: retry

Try / catch

answerSDP, err := session.CreateDownstreamAnswer(ctx)
if err != nil {
    if strings.Contains(err.Error(), "downstream WebRTC answer is empty") {
        log.Warn("empty downstream answer; retrying once with fresh peer connection")
        answerSDP, err = session.RecreateDownstreamAnswer(ctx)
    }
    if err != nil {
        return err
    }
}

Prevention

When it happens

Trigger: The downstream leg of AcceptUpstreamAnswer/CreateDownstreamAnswer in internal/client/codex/live/media.go running on a host with no usable ICE candidates, or the downstream peer connection being closed concurrently with negotiation.

Common situations: Same class as the upstream empty-offer error: fully blocked UDP/network stack, pion regression, or a concurrent Close() racing answer generation.

Related errors


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