{"record":{"id":"51b6dd42206c5176","repo":"router-for-me/CLIProxyAPI","slug":"gather-downstream-webrtc-candidates-w","errorCode":null,"errorMessage":"gather downstream WebRTC candidates: %w","messagePattern":"gather downstream WebRTC candidates: %w","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/client/codex/live/media.go","lineNumber":449,"sourceCode":"\t}); errRemote != nil {\n\t\terrSetRemote := fmt.Errorf(\"set upstream WebRTC answer: %w\", errRemote)\n\t\tif errClose := s.closeCandidateTunnels(); errClose != nil {\n\t\t\treturn \"\", errors.Join(errSetRemote, fmt.Errorf(\"close TCP candidate tunnels: %w\", errClose))\n\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:","sourceCodeStart":431,"sourceCodeEnd":467,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/client/codex/live/media.go#L431-L467","documentation":"Thrown when the context is cancelled while waiting for downstream ICE candidate gathering to finish (select on gatherComplete vs ctx.Done()), wrapping context.Canceled/DeadlineExceeded. The downstream answer has been committed but full candidate gathering did not complete before the caller gave up.","triggerScenarios":"Client disconnects or the request deadline expires during the final answer phase; gathering stalls when no usable network candidates exist on the downstream API (e.g. misconfigured PublicIP/NAT 1:1 settings or filtered interfaces).","commonSituations":"Short HTTP timeouts wrapping the answer endpoint; containers with restricted UDP where downstream host candidates gather slowly; PublicIP set to an unreachable address slowing gathering.","solutions":["Raise the context deadline for the answer call (gathering normally completes quickly)","Verify relay network settings: PublicIP correctness, UDP port range open in the firewall","Do not cancel the context while the client still awaits the answer","If gathering legitimately stalls, consider disabling mDNS candidate gathering in the SettingEngine for server deployments"],"exampleFix":"// before\nctx, cancel := context.WithTimeout(ctx, 1*time.Second)\nanswer, err := session.AcceptUpstreamAnswer(ctx, upstreamAnswerSDP)\n\n// after\nctx, cancel := context.WithTimeout(ctx, 15*time.Second)\nanswer, err := session.AcceptUpstreamAnswer(ctx, upstreamAnswerSDP)","handlingStrategy":"validation","validationCode":"if err := ctx.Err(); err != nil {\n\treturn \"\", err // skip answer work entirely on cancelled context\n}","typeGuard":null,"tryCatchPattern":"select {\ncase <-gatherComplete:\ncase <-ctx.Done():\n\tif errors.Is(ctx.Err(), context.DeadlineExceeded) {\n\t\tlog.Warn(\"downstream ICE gathering deadline exceeded; check relay PublicIP/UDP firewall\")\n\t}\n\treturn \"\", fmt.Errorf(\"gather downstream WebRTC candidates: %w\", ctx.Err())\n}","preventionTips":["Give the answer phase a deadline >=10s; gathering is normally sub-second","Open the configured UDP port range in the host firewall so host candidates gather promptly","Verify PublicIP points to a real routable address when NAT 1:1 is configured","Keep the client connected until the answer is returned"],"tags":["webrtc","ice","context","timeout","gathering","downstream"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}