router-for-me/CLIProxyAPI · error

marshal proxied upstream WebRTC answer: %w

Error message

marshal proxied upstream WebRTC answer: %w

What it means

After rewriting candidate addresses to the loopback tunnel listeners, the proxy re-serializes the SDP. pion/sdp's Marshal failing at this point is nearly impossible for a parsed-then-mutated document; it indicates the rewrite corrupted invariants. On failure the already-created tunnels are closed (closeTunnels) before returning.

Source

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

			closeTunnels()
			return "", nil, errTunnel
		}
		tunnels = append(tunnels, tunnel)
		listenerAddress, ok := tunnel.listener.Addr().(*net.TCPAddr)
		if !ok || listenerAddress.IP == nil {
			closeTunnels()
			return "", nil, errors.New("Codex live TCP proxy listener returned an invalid address")
		}
		fields := append([]string(nil), plan.fields...)
		fields[4] = listenerAddress.IP.String()
		fields[5] = strconv.Itoa(listenerAddress.Port)
		remoteDescription.MediaDescriptions[plan.mediaIndex].Attributes[plan.attributeIndex].Value = strings.Join(fields, " ")
	}

	rewritten, errMarshal := remoteDescription.Marshal()
	if errMarshal != nil {
		closeTunnels()
		return "", nil, fmt.Errorf("marshal proxied upstream WebRTC answer: %w", errMarshal)
	}
	return string(rewritten), tunnels, nil
}

func proxiedTCPCandidatePlan(rawCandidate string) (tcpCandidatePlan, bool, error) {
	trimmed := strings.TrimSpace(rawCandidate)
	candidate, errCandidate := ice.UnmarshalCandidate(trimmed)
	if errCandidate != nil {
		return tcpCandidatePlan{}, false, fmt.Errorf("parse upstream WebRTC candidate: %w", errCandidate)
	}
	if candidate.NetworkType() != ice.NetworkTypeTCP4 && candidate.NetworkType() != ice.NetworkTypeTCP6 {
		return tcpCandidatePlan{}, false, nil
	}
	if candidate.TCPType() != ice.TCPTypePassive {
		return tcpCandidatePlan{}, false, nil
	}
	if candidate.Component() != uint16(ice.ComponentRTP) || candidate.Type() != ice.CandidateTypeHost {
		return tcpCandidatePlan{}, false, nil

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Log the rewritten candidate lines before Marshal and check the substituted IP/port values are bare IPs and integers.
  2. If you modified the rewrite, ensure listenerAddress.IP.String() is not '[::1]' — use the IP without brackets for IPv6 loopback (netip's String() already omits them; a net.TCPAddr-based path may not).
  3. Reproduce with tcp_proxy_test.go fixtures; Marshal failures on unmodified code should be reported upstream with the SDP.
Defensive patterns

Strategy: try-catch

Try / catch

rewritten, tunnels, err := prepareProxiedUpstreamAnswer(answer, offer, dialer)
if err != nil {
	// tunnels are already closed by the callee on marshal failure
	if strings.Contains(err.Error(), "marshal proxied") {
		log.WithError(err).Error("SDP rewrite produced unmarshalable output")
	}
	return err
}

Prevention

When it happens

Trigger: remoteDescription.Marshal() fails after fields[4]/fields[5] of candidate lines were replaced — e.g. a rewritten value contains characters that break SDP attribute encoding, or the struct was left in an invalid state.

Common situations: Extremely rare in practice; most likely after local modifications to the rewrite logic (writing an IPv6 address with brackets into a field expecting a bare IP, or writing a non-numeric port).

Related errors


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