router-for-me/CLIProxyAPI · error

upstream WebRTC answer exceeds the %d candidate limit

Error message

upstream WebRTC answer exceeds the %d candidate limit

What it means

A defensive DoS cap: the proxy counts every a=candidate attribute in the upstream answer and hard-fails past maxUpstreamICECandidates (64). An answer with more than 64 candidates is treated as hostile or broken rather than being processed.

Source

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

		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 {
			attribute := media.Attributes[attributeIndex]
			if !attribute.IsICECandidate() {
				filtered = append(filtered, attribute)
				continue
			}
			candidateCount++
			if candidateCount > maxUpstreamICECandidates {
				return "", nil, fmt.Errorf("upstream WebRTC answer exceeds the %d candidate limit", maxUpstreamICECandidates)
			}
			plan, keep, errCandidate := proxiedTCPCandidatePlan(attribute.Value)
			if errCandidate != nil {
				return "", nil, errCandidate
			}
			if !keep {
				continue
			}
			if len(plans) >= maxProxiedTCPCandidates {
				return "", nil, fmt.Errorf("upstream WebRTC answer exceeds the %d TCP candidate proxy limit", maxProxiedTCPCandidates)
			}
			plan.mediaIndex = mediaIndex
			plan.attributeIndex = len(filtered)
			filtered = append(filtered, attribute)
			plans = append(plans, plan)
		}
		media.Attributes = filtered
	}

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Log the candidate count and inspect the answer; if legitimate, raise maxUpstreamICECandidates in tcp_proxy.go (it is a local const, currently 64).
  2. If unexpected, report the oversized SDP to the upstream service — normal Codex answers are far below 64 candidates.
  3. Rate-limit or reject the session at a higher layer if a peer repeatedly sends oversized answers.

Example fix

// before
maxUpstreamICECandidates = 64

// after (only if upstream legitimately sends more)
maxUpstreamICECandidates = 128
Defensive patterns

Strategy: validation

Validate before calling

// Cheap pre-count of candidates in the raw SDP
func candidateCount(sdp string) int { return strings.Count(sdp, "a=candidate:") + strings.Count(sdp, "a=candidate ") }
if candidateCount(answer) > maxUpstreamICECandidates { /* reject early */ }

Try / catch

if err != nil && strings.Contains(err.Error(), "candidate limit") {
	log.Warn("possible SDP flooding from upstream; dropping session")
	return errSessionDropped
}

Prevention

When it happens

Trigger: The upstream answer contains more than 64 ICE candidate attributes across all media sections (count includes UDP and non-TCP candidates).

Common situations: Upstream infrastructure change flooding SDP with candidates; a malicious/buggy peer; a fuzzing or load test feeding oversized SDP.

Related errors


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