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

  1. Log the exact candidate string that failed and validate its field layout against RFC 8445.
  2. Upgrade github.com/pion/ice — candidate parsing gains formats across releases.
  3. 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

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


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