router-for-me/CLIProxyAPI · error

parse upstream WebRTC offer for TCP proxy: %w

Error message

parse upstream WebRTC offer for TCP proxy: %w

What it means

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.

Source

Thrown at internal/client/codex/live/tcp_proxy.go:101

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 {
			continue
		}
		filtered := make([]sdp.Attribute, 0, len(media.Attributes))
		for attributeIndex := range media.Attributes {

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Check that s.localOffer is set from the actual local peer connection's local description before the answer arrives.
  2. Log len(localOffer) and its first line to verify it starts with 'v=0'.
  3. Fix the session lifecycle bug that leaves the offer empty or stale.

Example fix

// before
rewrittenAnswer, tunnels, errProxy := prepareProxiedUpstreamAnswer(upstreamAnswer, s.localOffer, s.proxyDialer)

// after: fail fast on an uninitialized offer
if strings.TrimSpace(s.localOffer) == "" {
	return errors.New("local offer is not initialized before upstream answer")
}
rewrittenAnswer, tunnels, errProxy := prepareProxiedUpstreamAnswer(upstreamAnswer, s.localOffer, s.proxyDialer)
Defensive patterns

Strategy: validation

Validate before calling

if strings.TrimSpace(s.localOffer) == "" || !strings.HasPrefix(strings.TrimSpace(s.localOffer), "v=") {
	return errors.New("local offer missing or not SDP; cannot prepare TCP proxy")
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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