router-for-me/CLIProxyAPI · error
set upstream WebRTC answer: %w close TCP candidate tunnels:
Error message
set upstream WebRTC answer: %w close TCP candidate tunnels: %w
What it means
The errors.Join composite returned when SetRemoteDescription of the upstream answer fails AND the subsequent rollback (closing the installed TCP candidate tunnels) also fails. The first '%w' is the upstream answer failure (see 335); the second is the tunnel-close failure. Both errors are preserved so the real root cause (usually the SDP mismatch) is readable from the joined chain.
Source
Thrown at internal/client/codex/live/media.go:434
for _, tunnel := range tunnels {
tunnel.setForwardingStartedHandler(s.logForwardingStarted)
}
if !s.installCandidateTunnels(tunnels) {
errClosed := errors.New("Codex live media session closed while configuring TCP proxy")
if errClose := closeCandidateTunnels(tunnels); errClose != nil {
return "", errors.Join(errClosed, fmt.Errorf("close TCP candidate tunnels: %w", errClose))
}
return "", errClosed
}
answerToApply = rewrittenAnswer
}
if errRemote := s.upstream.SetRemoteDescription(webrtc.SessionDescription{
Type: webrtc.SDPTypeAnswer,
SDP: answerToApply,
}); errRemote != nil {
errSetRemote := fmt.Errorf("set upstream WebRTC answer: %w", errRemote)
if errClose := s.closeCandidateTunnels(); errClose != nil {
return "", errors.Join(errSetRemote, fmt.Errorf("close TCP candidate tunnels: %w", errClose))
}
return "", errSetRemote
}
gatherComplete := webrtc.GatheringCompletePromise(s.downstream)
answer, errAnswer := s.downstream.CreateAnswer(nil)
if errAnswer != nil {
return "", fmt.Errorf("create downstream WebRTC answer: %w", errAnswer)
}
if errLocal := s.downstream.SetLocalDescription(answer); errLocal != nil {
return "", fmt.Errorf("set downstream WebRTC answer: %w", errLocal)
}
select {
case <-gatherComplete:
case <-ctx.Done():
return "", fmt.Errorf("gather downstream WebRTC candidates: %w", ctx.Err())
}
localDescription := s.downstream.LocalDescription()
if localDescription == nil || strings.TrimSpace(localDescription.SDP) == "" {View on GitHub (pinned to 78f0c4079e)
Solutions
- Unwrap with errors.Is to identify the primary cause (the answer SDP error) and fix per error 335
- Prevent the teardown race that double-closes tunnels
- Report the joined error verbatim in logs — do not collapse it, the second error is diagnostic
Example fix
// before
if err := session.AcceptUpstreamAnswer(ctx, answer); err != nil {
log.Errorf("answer failed: %v", err)
}
// after
if err := session.AcceptUpstreamAnswer(ctx, answer); err != nil {
var sdpErr *webrtc.InvalidStateError
log.Errorf("answer failed: %v (sdp-state=%v)", err, errors.As(err, &sdpErr))
} Defensive patterns
Strategy: try-catch
Try / catch
if _, err := session.AcceptUpstreamAnswer(ctx, answer); err != nil {
for unwindErr := err; unwindErr != nil; {
if joined, ok := unwindErr.(interface{ Unwrap() []error }); ok {
for _, inner := range joined.Unwrap() {
log.WithError(inner).Debug("joined failure component")
}
break
}
unwindErr = errors.Unwrap(unwindErr)
}
} Prevention
- Preserve the joined chain — each component localizes a different failure (SDP vs tunnel close)
- Never retry on a session that failed here; create a new session instead
- Log both components with errors.Is/As classification
When it happens
Trigger: Same as 335 on the proxied path (tunnels were installed), plus closeCandidateTunnels erroring — typically because the session's Close already ran and the tunnels were already closed.
Common situations: Proxied deployment where the answer application fails while a concurrent teardown closes tunnels; inspect with errors.Is/As on the joined chain.
Related errors
- close TCP candidate tunnels: %w
- upstream WebRTC offer is empty
- downstream WebRTC answer is empty
- upstream WebRTC answer has no supported public TCP passive c
- upstream WebRTC TCP proxy candidate address must be an IP
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/b9a8e8403bfc8e02.
Report an issue: GitHub.