router-for-me/CLIProxyAPI · error
read upstream WebRTC offer ICE credentials: %w
Error message
read upstream WebRTC offer ICE credentials: %w
What it means
Identical credential extraction, but for the local offer SDP. The proxy needs the local ICE password to validate that connections arriving on the loopback tunnel listener genuinely come from the local ICE agent. If the local offer lacks ICE credentials, the proxy cannot secure the tunnels and refuses to continue.
Source
Thrown at internal/client/codex/live/tcp_proxy.go:109
func prepareProxiedUpstreamAnswer(answer, localOffer string, dialer proxy.ContextDialer) (string, []*tcpCandidateTunnel, error) {
if dialer == nil {
return "", nil, errors.New("Codex live TCP proxy dialer is unavailable")
}
var remoteDescription sdp.SessionDescription
if errUnmarshal := remoteDescription.UnmarshalString(answer); errUnmarshal != nil {
return "", nil, fmt.Errorf("parse upstream WebRTC answer for TCP proxy: %w", errUnmarshal)
}
var localDescription sdp.SessionDescription
if errUnmarshal := localDescription.UnmarshalString(localOffer); errUnmarshal != nil {
return "", nil, fmt.Errorf("parse upstream WebRTC offer for TCP proxy: %w", errUnmarshal)
}
remoteCredentials, errCredentials := bundledICECredentials(&remoteDescription)
if errCredentials != nil {
return "", nil, fmt.Errorf("read upstream WebRTC answer ICE credentials: %w", errCredentials)
}
localCredentials, errCredentials := bundledICECredentials(&localDescription)
if errCredentials != nil {
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)View on GitHub (pinned to 78f0c4079e)
Solutions
- Verify the stored localOffer is the localDescription.SDP of the real RTCPeerConnection.
- Inspect it for a=ice-ufrag/a=ice-pwd lines; if absent, fix the offer generation path.
- Add a unit test asserting bundledICECredentials succeeds on the offer you actually store.
Defensive patterns
Strategy: validation
Validate before calling
if !strings.Contains(s.localOffer, "a=ice-pwd:") {
return errors.New("local offer has no ICE password; regenerate offer")
} Prevention
- Always source the offer from a real RTCPeerConnection local description.
- Never handcraft or round-trip the offer through lossy serialization.
When it happens
Trigger: localOffer parses as SDP but has no readable ice-ufrag/ice-pwd in its media descriptions — empty/placeholder offer, offer generated by a stack that omits ICE attributes, or the wrong string (e.g. a candidate list) was stored as the offer.
Common situations: Local offer built manually for testing without ICE attributes; session state populated from a different field; refactor changed which string is kept as the offer.
Related errors
- parse upstream WebRTC offer for TCP proxy: %w
- read upstream WebRTC answer ICE credentials: %w
- SDP contains incomplete ICE credentials
- SDP contains inconsistent bundled ICE credentials
- parse upstream WebRTC answer for TCP proxy: %w
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/0fa29772733a7515.
Report an issue: GitHub.