router-for-me/CLIProxyAPI · error

create downstream WebRTC answer: %w

Error message

create downstream WebRTC answer: %w

What it means

Thrown when downstream.CreateAnswer(nil) fails generating the SDP answer returned to the local client. By this point the client offer was applied (SetRemoteDescription succeeded) and the Opus track was added, so failure means the downstream PC cannot produce an answer — state is wrong (closed/closing), or the negotiated media cannot be marshaled. Often the downstream leg was torn down while the upstream leg was being set up.

Source

Thrown at internal/client/codex/live/media.go:441

			}
			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) == "" {
		return "", errors.New("downstream WebRTC answer is empty")
	}
	return localDescription.SDP, nil
}

func (s *pionMediaSession) installCandidateTunnels(tunnels []*tcpCandidateTunnel) bool {
	if s == nil {

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Reduce upstream answer latency (proxy hop adds the most)
  2. Retry the full session when the client reconnects
  3. Guard against calling AcceptUpstreamAnswer after the downstream PC state is 'closed' (check ICEConnectionState)
Defensive patterns

Strategy: retry

Validate before calling

func downstreamStillLive(s *pionMediaSession) bool {
	return s.downstream != nil &&
		s.downstream.ConnectionState() != webrtc.PeerConnectionStateClosed &&
		s.downstream.ConnectionState() != webrtc.PeerConnectionStateFailed
}

Try / catch

if _, err := session.AcceptUpstreamAnswer(ctx, answer); err != nil {
	if strings.Contains(err.Error(), "create downstream WebRTC answer") {
		// downstream leg died during upstream round-trip; full session retry is the only recovery
		session.CloseWithReason("downstream_lost")
	}
}

Prevention

When it happens

Trigger: Downstream PC closed concurrently (client disconnected during the upstream answer round-trip), or an SDP generation failure after the offer/track setup.

Common situations: Slow upstream negotiation exceeding the client's patience; the client closes its PC and the relay's downstream PC transitions to closed before CreateAnswer.

Related errors


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