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
- Check logs for the preceding 'create downstream WebRTC answer' / 'set downstream WebRTC answer' / 'gather downstream WebRTC candidates' errors which usually carry the real cause
- Verify codex.live-media-relay UDP port range and public-ip settings so the relay can bind and advertise candidates
- Confirm nothing closed the downstream peer connection concurrently (session teardown, context cancel)
- 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
- Validate the relay's UDP port range and public-ip before enabling live media
- Avoid tearing down sessions concurrently with negotiation
- Track pion/webrtc module versions in upgrades; regression-test the media path
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
- upstream WebRTC offer is empty
- Codex live media session closed while configuring TCP proxy
- upstream WebRTC answer has no supported public TCP passive c
- Codex live TCP proxy listener returned an invalid address
- upstream WebRTC TCP proxy candidate address must be an IP
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/e6a38999b24befe4.
Report an issue: GitHub.