router-for-me/CLIProxyAPI · error

parse upstream WebRTC answer for TCP proxy

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.

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.

Appendix: 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)