{"record":{"id":"e6a38999b24befe4","repo":"router-for-me/CLIProxyAPI","slug":"downstream-webrtc-answer-is-empty","errorCode":null,"errorMessage":"downstream WebRTC answer is empty","messagePattern":"downstream WebRTC answer is empty","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/client/codex/live/media.go","lineNumber":453,"sourceCode":"\t\t}\n\t\treturn \"\", errSetRemote\n\t}\n\tgatherComplete := webrtc.GatheringCompletePromise(s.downstream)\n\tanswer, errAnswer := s.downstream.CreateAnswer(nil)\n\tif errAnswer != nil {\n\t\treturn \"\", fmt.Errorf(\"create downstream WebRTC answer: %w\", errAnswer)\n\t}\n\tif errLocal := s.downstream.SetLocalDescription(answer); errLocal != nil {\n\t\treturn \"\", fmt.Errorf(\"set downstream WebRTC answer: %w\", errLocal)\n\t}\n\tselect {\n\tcase <-gatherComplete:\n\tcase <-ctx.Done():\n\t\treturn \"\", fmt.Errorf(\"gather downstream WebRTC candidates: %w\", ctx.Err())\n\t}\n\tlocalDescription := s.downstream.LocalDescription()\n\tif localDescription == nil || strings.TrimSpace(localDescription.SDP) == \"\" {\n\t\treturn \"\", errors.New(\"downstream WebRTC answer is empty\")\n\t}\n\treturn localDescription.SDP, nil\n}\n\nfunc (s *pionMediaSession) installCandidateTunnels(tunnels []*tcpCandidateTunnel) bool {\n\tif s == nil {\n\t\treturn false\n\t}\n\ts.tunnelsMu.Lock()\n\tdefer s.tunnelsMu.Unlock()\n\tselect {\n\tcase <-s.done:\n\t\treturn false\n\tdefault:\n\t}\n\ts.tunnels = tunnels\n\treturn true\n}","sourceCodeStart":435,"sourceCodeEnd":471,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/client/codex/live/media.go#L435-L471","documentation":"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.","triggerScenarios":"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.","commonSituations":"Same class as the upstream empty-offer error: fully blocked UDP/network stack, pion regression, or a concurrent Close() racing answer generation.","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"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"answerSDP, err := session.CreateDownstreamAnswer(ctx)\nif err != nil {\n    if strings.Contains(err.Error(), \"downstream WebRTC answer is empty\") {\n        log.Warn(\"empty downstream answer; retrying once with fresh peer connection\")\n        answerSDP, err = session.RecreateDownstreamAnswer(ctx)\n    }\n    if err != nil {\n        return err\n    }\n}","preventionTips":["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"],"tags":["webrtc","codex","media-relay","pion","sdp"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}