router-for-me/CLIProxyAPI · error
upstream WebRTC offer is empty
Error message
upstream WebRTC offer is empty
What it means
Thrown by the Codex live media relay after the pion WebRTC engine finished gathering ICE candidates for the upstream offer, but LocalDescription() returned nil or an SDP consisting only of whitespace. It means the local side of the session never produced a usable offer, so there is nothing to send to the upstream Codex media endpoint. The session is closed before returning. This is an internal invariant failure of the pion stack rather than a network error.
Source
Thrown at internal/client/codex/live/media.go:400
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 {
return "", errProxy
}
for _, tunnel := range tunnels {
tunnel.setForwardingStartedHandler(s.logForwardingStarted)
}View on GitHub (pinned to 78f0c4079e)
Solutions
- Check surrounding logs for the earlier 'set upstream WebRTC offer' or 'gather upstream WebRTC candidates' errors — those are usually the root cause
- Verify the machine has at least one usable network interface and that codex.live-media-relay UDP settings/ICE servers allow candidate gathering
- Reproduce with debug logging enabled for the pion stack (environment variable or logrus level) to see whether any candidates were generated
- If it recurs consistently, pin/upgrade the pion/webrtc module version — an empty LocalDescription after successful SetLocalDescription indicates an engine bug
Defensive patterns
Strategy: retry
Try / catch
resp, err := live.NewUpstreamSession(ctx)
if err != nil {
if strings.Contains(err.Error(), "upstream WebRTC offer is empty") {
log.Warn("empty upstream offer; retrying media negotiation once")
time.Sleep(backoff)
resp, err = live.NewUpstreamSession(ctx)
}
if err != nil {
return err // surface to client as 502/503
}
} Prevention
- Keep codex.live-media-relay UDP settings valid so ICE gathering always has usable candidates
- Log pion/ice debug output in staging to detect candidate-starved environments before production
- Treat repeated occurrences as a pion version regression and pin a known-good module version
When it happens
Trigger: Calling the media session factory (NewMediaSession / equivalent dial path in internal/client/codex/live/media.go) where SetLocalDescription technically succeeded but the engine emitted an empty SDP, or ICE gathering completed immediately with zero candidates and no base description. Can also happen if the peer connection was closed concurrently so LocalDescription() returns nil.
Common situations: Severe network restriction where the host has no interfaces/candidates at all, a pion version regression, or a session torn down by a concurrent goroutine (context cancel racing the gather-complete signal).
Related errors
- downstream WebRTC answer 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/1df2ad54406a677f.
Report an issue: GitHub.