{"record":{"id":"f5b95e1bd79d09fe","repo":"router-for-me/CLIProxyAPI","slug":"parse-upstream-webrtc-offer-for-tcp-proxy-w","errorCode":null,"errorMessage":"parse upstream WebRTC offer for TCP proxy: %w","messagePattern":"parse upstream WebRTC offer for TCP proxy: %w","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/client/codex/live/tcp_proxy.go","lineNumber":101,"sourceCode":"\ntype tcpCandidatePlan struct {\n\tmediaIndex     int\n\tattributeIndex int\n\tfields         []string\n\ttarget         netip.AddrPort\n}\n\nfunc prepareProxiedUpstreamAnswer(answer, localOffer string, dialer proxy.ContextDialer) (string, []*tcpCandidateTunnel, error) {\n\tif dialer == nil {\n\t\treturn \"\", nil, errors.New(\"Codex live TCP proxy dialer is unavailable\")\n\t}\n\tvar remoteDescription sdp.SessionDescription\n\tif errUnmarshal := remoteDescription.UnmarshalString(answer); errUnmarshal != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"parse upstream WebRTC answer for TCP proxy: %w\", errUnmarshal)\n\t}\n\tvar localDescription sdp.SessionDescription\n\tif errUnmarshal := localDescription.UnmarshalString(localOffer); errUnmarshal != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"parse upstream WebRTC offer for TCP proxy: %w\", errUnmarshal)\n\t}\n\tremoteCredentials, errCredentials := bundledICECredentials(&remoteDescription)\n\tif errCredentials != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"read upstream WebRTC answer ICE credentials: %w\", errCredentials)\n\t}\n\tlocalCredentials, errCredentials := bundledICECredentials(&localDescription)\n\tif errCredentials != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"read upstream WebRTC offer ICE credentials: %w\", errCredentials)\n\t}\n\n\tplans := make([]tcpCandidatePlan, 0, 4)\n\tcandidateCount := 0\n\tfor mediaIndex, media := range remoteDescription.MediaDescriptions {\n\t\tif media == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfiltered := make([]sdp.Attribute, 0, len(media.Attributes))\n\t\tfor attributeIndex := range media.Attributes {","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/client/codex/live/tcp_proxy.go#L83-L119","documentation":"Same rewrite pipeline as the answer parse, but for the LOCAL offer SDP that this client generated. If the local offer cannot be parsed back, the proxy cannot read the local ICE credentials needed to authenticate tunnel connections, so it fails before dialing anything.","triggerScenarios":"prepareProxiedUpstreamAnswer is called with a localOffer string that fails sdp.SessionDescription.UnmarshalString — typically an empty string, a truncated offer, or a non-SDP value passed by the caller (media session stored something other than the raw SDP).","commonSituations":"Session state was not initialized before the upstream answer arrived (localOffer empty); the offer was serialized/deserialized through a layer that escaped or truncated it; race where the session was reset mid-handshake.","solutions":["Check that s.localOffer is set from the actual local peer connection's local description before the answer arrives.","Log len(localOffer) and its first line to verify it starts with 'v=0'.","Fix the session lifecycle bug that leaves the offer empty or stale."],"exampleFix":"// before\nrewrittenAnswer, tunnels, errProxy := prepareProxiedUpstreamAnswer(upstreamAnswer, s.localOffer, s.proxyDialer)\n\n// after: fail fast on an uninitialized offer\nif strings.TrimSpace(s.localOffer) == \"\" {\n\treturn errors.New(\"local offer is not initialized before upstream answer\")\n}\nrewrittenAnswer, tunnels, errProxy := prepareProxiedUpstreamAnswer(upstreamAnswer, s.localOffer, s.proxyDialer)","handlingStrategy":"validation","validationCode":"if strings.TrimSpace(s.localOffer) == \"\" || !strings.HasPrefix(strings.TrimSpace(s.localOffer), \"v=\") {\n\treturn errors.New(\"local offer missing or not SDP; cannot prepare TCP proxy\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Initialize and store localDescription.SDP before subscribing to answer events.","Add a session-state unit test that asserts the offer is non-empty at answer time."],"tags":["webrtc","sdp","codex-live","tcp-proxy","session-state"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}