router-for-me/CLIProxyAPI · error
parse upstream WebRTC candidate: %w
Error message
parse upstream WebRTC candidate: %w
What it means
Each a=candidate line in the answer is parsed with pion/ice's UnmarshalCandidate. This error means at least one candidate line is syntactically invalid under RFC 8445 (wrong field count, unknown foundation quirks, bad priority, etc.), and the proxy treats a single bad candidate as fatal rather than skipping it.
Source
Thrown at internal/client/codex/live/tcp_proxy.go:189
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
}
if candidate.Port() != 443 {
return tcpCandidatePlan{}, false, fmt.Errorf("upstream WebRTC TCP proxy candidate uses disallowed port %d", candidate.Port())
}
address, errAddress := netip.ParseAddr(candidate.Address())
if errAddress != nil {
return tcpCandidatePlan{}, false, errors.New("upstream WebRTC TCP proxy candidate address must be an IP")
}
address = address.Unmap()View on GitHub (pinned to 78f0c4079e)
Solutions
- Log the exact candidate string that failed and validate its field layout against RFC 8445.
- Upgrade github.com/pion/ice — candidate parsing gains formats across releases.
- If the upstream format is stable and merely unsupported, pre-normalize the candidate string (strip prefixes, fix spacing) before UnmarshalCandidate.
Defensive patterns
Strategy: try-catch
Validate before calling
// Skip candidates that are obviously malformed before calling the proxy
func isPlausibleCandidate(line string) bool {
f := strings.Fields(strings.TrimPrefix(line, "a=candidate:"))
return len(f) >= 8
} Try / catch
if err != nil && strings.Contains(err.Error(), "parse upstream WebRTC candidate") {
log.WithError(err).Warn("skipping session: upstream emitted unparsable candidate")
return err
} Prevention
- Keep pion/ice current; candidate grammar support improves between releases.
- Capture the failing candidate string in logs to distinguish upstream change vs corruption.
When it happens
Trigger: proxiedTCPCandidatePlan(attribute.Value) is called for every candidate attribute; ice.UnmarshalCandidate fails on one — e.g. a candidate with fewer than the required fields, a non-numeric component, or an extension format pion/ice rejects.
Common situations: Upstream emits a candidate dialect pion/ice does not accept (e.g. odd extensions or 'a=candidate:' prefix handling); SDP was mutated/truncated in transit; older pion/ice version lacking newer candidate features.
Related errors
- parse upstream WebRTC answer for TCP proxy: %w
- read upstream WebRTC answer ICE credentials: %w
- read upstream WebRTC offer ICE credentials: %w
- upstream WebRTC TCP proxy candidate is malformed
- SDP contains incomplete ICE credentials
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/8b74f86c91f4b56d.
Report an issue: GitHub.