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, nilView on GitHub (pinned to 78f0c4079e)
Solutions
- Log the rewritten candidate lines before Marshal and check the substituted IP/port values are bare IPs and integers.
- 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).
- 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 modifying the rewrite, only substitute bare IPs and integer ports into candidate fields.
- Unit-test the full parse->rewrite->marshal round trip on realistic SDP fixtures.
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
- parse upstream WebRTC answer for TCP proxy: %w
- parse upstream WebRTC offer for TCP proxy: %w
- read upstream WebRTC answer ICE credentials: %w
- read upstream WebRTC offer ICE credentials: %w
- upstream WebRTC answer has no supported public TCP passive c
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/8ddb9a6dfadfccf0.
Report an issue: GitHub.