{"record":{"id":"803978973db7625c","repo":"router-for-me/CLIProxyAPI","slug":"parse-upstream-webrtc-answer-for-tcp-proxy-w","errorCode":null,"errorMessage":"parse upstream WebRTC answer for TCP proxy: %w","messagePattern":"parse upstream WebRTC answer for TCP proxy: %w","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/client/codex/live/tcp_proxy.go","lineNumber":97,"sourceCode":"\tonForwardingStarted func()\n\tctx                 context.Context\n\tcancel              context.CancelFunc\n}\n\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 {","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/client/codex/live/tcp_proxy.go#L79-L115","documentation":"The Codex live TCP proxy rewrites the upstream WebRTC answer SDP before handing it to the local ICE agent. This error means pion/sdp's UnmarshalString rejected the answer string the upstream (Codex live media) sent, so the SDP is structurally malformed (bad line endings, truncated type lines, or non-SDP payload). The proxy aborts and no TCP candidate tunnels are created.","triggerScenarios":"prepareProxiedUpstreamAnswer(answer, localOffer, dialer) is called (media.go:412) after the upstream answers the offer; the answer string fails sdp.SessionDescription.UnmarshalString — e.g. missing 'v=' line, CRLF issues, or the server returned an error body instead of SDP.","commonSituations":"Upstream API change or A/B behavior returning JSON instead of SDP; a proxy/middlebox truncating the WebSocket message; version skew between the server's WebRTC stack and the pion/sdp parser used here.","solutions":["Log the raw answer string (redacted) at debug level and inspect the first lines to confirm it is actually SDP ('v=0', 'm=...') and not an error payload.","If the payload is an upstream error, fix the request/session state that caused the upstream to reject the offer.","If the SDP is merely non-conformant (line endings, ordering), normalize it (ensure CRLF, strip leading/trailing whitespace) before calling the proxy.","Update/patch pion/sdp to a version matching the upstream's SDP generation."],"exampleFix":"// before\nvar remoteDescription sdp.SessionDescription\nif errUnmarshal := remoteDescription.UnmarshalString(answer); errUnmarshal != nil {\n\treturn \"\", nil, fmt.Errorf(\"parse upstream WebRTC answer for TCP proxy: %w\", errUnmarshal)\n}\n\n// after: normalize line endings and trim before parsing\nnormalized := strings.ReplaceAll(strings.TrimSpace(answer), \"\\r\\n\", \"\\n\")\nvar remoteDescription sdp.SessionDescription\nif errUnmarshal := remoteDescription.UnmarshalString(normalized); errUnmarshal != nil {\n\treturn \"\", nil, fmt.Errorf(\"parse upstream WebRTC answer for TCP proxy: %w\", errUnmarshal)\n}","handlingStrategy":"try-catch","validationCode":"// Pre-flight: does the answer look like SDP before invoking the proxy?\nfunc looksLikeSDP(s string) bool {\n\tt := strings.TrimSpace(s)\n\treturn strings.HasPrefix(t, \"v=\") && strings.Contains(t, \"\\nm=\") || strings.Contains(t, \"\\r\\nm=\")\n}","typeGuard":null,"tryCatchPattern":"rewritten, tunnels, err := prepareProxiedUpstreamAnswer(answer, offer, dialer)\nif err != nil {\n\tif strings.Contains(err.Error(), \"parse upstream WebRTC answer\") {\n\t\tlog.WithError(err).Warn(\"upstream sent malformed SDP answer; retrying session\")\n\t\treturn retrySession() // or fail the session with a user-facing message\n\t}\n\treturn err\n}","preventionTips":["Treat upstream answers as untrusted input: validate they start with 'v=' before parsing.","Log SDP payloads (redacted) at debug level so malformed answers can be diagnosed post-hoc.","Pin the upstream API version your client is tested against."],"tags":["webrtc","sdp","codex-live","tcp-proxy","parsing"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}