router-for-me/CLIProxyAPI · error
parse upstream WebRTC answer for TCP proxy: %w
Error message
parse upstream WebRTC answer for TCP proxy: %w
What it means
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.
Source
Thrown at internal/client/codex/live/tcp_proxy.go:97
onForwardingStarted func()
ctx context.Context
cancel context.CancelFunc
}
type tcpCandidatePlan struct {
mediaIndex int
attributeIndex int
fields []string
target netip.AddrPort
}
func prepareProxiedUpstreamAnswer(answer, localOffer string, dialer proxy.ContextDialer) (string, []*tcpCandidateTunnel, error) {
if dialer == nil {
return "", nil, errors.New("Codex live TCP proxy dialer is unavailable")
}
var remoteDescription sdp.SessionDescription
if errUnmarshal := remoteDescription.UnmarshalString(answer); errUnmarshal != nil {
return "", nil, fmt.Errorf("parse upstream WebRTC answer for TCP proxy: %w", errUnmarshal)
}
var localDescription sdp.SessionDescription
if errUnmarshal := localDescription.UnmarshalString(localOffer); errUnmarshal != nil {
return "", nil, fmt.Errorf("parse upstream WebRTC offer for TCP proxy: %w", errUnmarshal)
}
remoteCredentials, errCredentials := bundledICECredentials(&remoteDescription)
if errCredentials != nil {
return "", nil, fmt.Errorf("read upstream WebRTC answer ICE credentials: %w", errCredentials)
}
localCredentials, errCredentials := bundledICECredentials(&localDescription)
if errCredentials != nil {
return "", nil, fmt.Errorf("read upstream WebRTC offer ICE credentials: %w", errCredentials)
}
plans := make([]tcpCandidatePlan, 0, 4)
candidateCount := 0
for mediaIndex, media := range remoteDescription.MediaDescriptions {
if media == nil {View on GitHub (pinned to 78f0c4079e)
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.
Example fix
// before
var remoteDescription sdp.SessionDescription
if errUnmarshal := remoteDescription.UnmarshalString(answer); errUnmarshal != nil {
return "", nil, fmt.Errorf("parse upstream WebRTC answer for TCP proxy: %w", errUnmarshal)
}
// after: normalize line endings and trim before parsing
normalized := strings.ReplaceAll(strings.TrimSpace(answer), "\r\n", "\n")
var remoteDescription sdp.SessionDescription
if errUnmarshal := remoteDescription.UnmarshalString(normalized); errUnmarshal != nil {
return "", nil, fmt.Errorf("parse upstream WebRTC answer for TCP proxy: %w", errUnmarshal)
} Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight: does the answer look like SDP before invoking the proxy?
func looksLikeSDP(s string) bool {
t := strings.TrimSpace(s)
return strings.HasPrefix(t, "v=") && strings.Contains(t, "\nm=") || strings.Contains(t, "\r\nm=")
} Try / catch
rewritten, tunnels, err := prepareProxiedUpstreamAnswer(answer, offer, dialer)
if err != nil {
if strings.Contains(err.Error(), "parse upstream WebRTC answer") {
log.WithError(err).Warn("upstream sent malformed SDP answer; retrying session")
return retrySession() // or fail the session with a user-facing message
}
return err
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- upstream WebRTC TCP proxy candidate is malformed
- parse upstream WebRTC offer for TCP proxy: %w
- read upstream WebRTC answer ICE credentials: %w
- read upstream WebRTC offer ICE credentials: %w
- marshal proxied upstream WebRTC answer: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/803978973db7625c.
Report an issue: GitHub.