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

  1. Unwrap with errors.Is to identify the primary cause (the answer SDP error) and fix per error 335
  2. Prevent the teardown race that double-closes tunnels
  3. 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

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


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