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

  1. 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.
  2. If the payload is an upstream error, fix the request/session state that caused the upstream to reject the offer.
  3. If the SDP is merely non-conformant (line endings, ordering), normalize it (ensure CRLF, strip leading/trailing whitespace) before calling the proxy.
  4. 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

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


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/803978973db7625c. Report an issue: GitHub.