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
- Reduce upstream answer latency (proxy hop adds the most)
- Retry the full session when the client reconnects
- 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
- Minimize upstream answer latency so the downstream PC stays live
- Check downstream connection state before applying the upstream answer
- Recover only via a brand-new session, never by reusing this one
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
- set downstream WebRTC answer: %w
- create upstream WebRTC offer: %w
- set upstream WebRTC offer: %w
- set upstream WebRTC answer: %w
- upstream WebRTC offer is empty
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/9b4e68b7f705e87c.
Report an issue: GitHub.