{"record":{"id":"1df2ad54406a677f","repo":"router-for-me/CLIProxyAPI","slug":"upstream-webrtc-offer-is-empty","errorCode":null,"errorMessage":"upstream WebRTC offer is empty","messagePattern":"upstream WebRTC offer is empty","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/client/codex/live/media.go","lineNumber":400,"sourceCode":"\toffer, errOffer := upstream.CreateOffer(nil)\n\tif errOffer != nil {\n\t\t_ = session.Close()\n\t\treturn nil, \"\", fmt.Errorf(\"create upstream WebRTC offer: %w\", errOffer)\n\t}\n\tif errLocal := upstream.SetLocalDescription(offer); errLocal != nil {\n\t\t_ = session.Close()\n\t\treturn nil, \"\", fmt.Errorf(\"set upstream WebRTC offer: %w\", errLocal)\n\t}\n\tselect {\n\tcase <-gatherComplete:\n\tcase <-ctx.Done():\n\t\t_ = session.Close()\n\t\treturn nil, \"\", fmt.Errorf(\"gather upstream WebRTC candidates: %w\", ctx.Err())\n\t}\n\tlocalDescription := upstream.LocalDescription()\n\tif localDescription == nil || strings.TrimSpace(localDescription.SDP) == \"\" {\n\t\t_ = session.Close()\n\t\treturn nil, \"\", errors.New(\"upstream WebRTC offer is empty\")\n\t}\n\tsession.localOffer = localDescription.SDP\n\treturn session, localDescription.SDP, nil\n}\n\nfunc (s *pionMediaSession) AcceptUpstreamAnswer(ctx context.Context, upstreamAnswer string) (string, error) {\n\tif s == nil || s.upstream == nil || s.downstream == nil {\n\t\treturn \"\", errors.New(\"Codex live media session unavailable\")\n\t}\n\tanswerToApply := upstreamAnswer\n\tif s.proxyDialer != nil {\n\t\trewrittenAnswer, tunnels, errProxy := prepareProxiedUpstreamAnswer(upstreamAnswer, s.localOffer, s.proxyDialer)\n\t\tif errProxy != nil {\n\t\t\treturn \"\", errProxy\n\t\t}\n\t\tfor _, tunnel := range tunnels {\n\t\t\ttunnel.setForwardingStartedHandler(s.logForwardingStarted)\n\t\t}","sourceCodeStart":382,"sourceCodeEnd":418,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/client/codex/live/media.go#L382-L418","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp, err := live.NewUpstreamSession(ctx)\nif err != nil {\n    if strings.Contains(err.Error(), \"upstream WebRTC offer is empty\") {\n        log.Warn(\"empty upstream offer; retrying media negotiation once\")\n        time.Sleep(backoff)\n        resp, err = live.NewUpstreamSession(ctx)\n    }\n    if err != nil {\n        return err // surface to client as 502/503\n    }\n}","preventionTips":["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"],"tags":["webrtc","codex","media-relay","pion","sdp"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}