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

  1. Check surrounding logs for the earlier 'set upstream WebRTC offer' or 'gather upstream WebRTC candidates' errors — those are usually the root cause
  2. Verify the machine has at least one usable network interface and that codex.live-media-relay UDP settings/ICE servers allow candidate gathering
  3. Reproduce with debug logging enabled for the pion stack (environment variable or logrus level) to see whether any candidates were generated
  4. 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

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


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