router-for-me/CLIProxyAPI · error
create upstream WebRTC offer
Error message
create upstream WebRTC offer: %w
What it means
Thrown when upstream.CreateOffer(nil) fails while generating the SDP offer to send to OpenAI. Pion CreateOffer errors when the connection state disallows offering (already closed/closing), when there are no media or data channels to describe, or on SDP marshaling failure. Here the DataChannel and audio track were just added, so state races dominate.
Solutions
- Retry the full session (new NewSession) when the client remains connected
- Audit for goroutines that call session.Close() during setup (bridge failure handler fires 'data_channel_failed')
- Align pion/webrtc version to pick up offer-generation fixes
Defensive patterns
Strategy: retry
Try / catch
offer, errOffer := upstream.CreateOffer(nil)
if errOffer != nil {
if pcState := upstream.ConnectionState(); pcState == webrtc.PeerConnectionStateClosed {
return nil, "", fmt.Errorf("session closed before offer: %w", errOffer)
}
// transient: retry once after brief backoff
time.Sleep(50 * time.Millisecond)
offer, errOffer = upstream.CreateOffer(nil)
} Prevention
- Check upstream.ConnectionState() before offer creation in custom flows
- Serialize setup steps with session lifecycle (done channel) to avoid closed-state offers
- Retry at session granularity in callers
When it happens
Trigger: Upstream PC entered 'closed' state concurrently (session.Close raced setup), or internal SDP generation failure after adding the track/channel.
Common situations: Client disconnects mid-handshake causing teardown to race offer creation; rare pion internal errors.
Related errors
- create downstream WebRTC answer
- set downstream WebRTC answer
- set upstream WebRTC offer
- close TCP candidate tunnels
- create upstream DataChannel
AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15).
Data as JSON: /api/errors/4dfecf1044548402.
Report an issue: GitHub.
Appendix: source
Thrown at internal/client/codex/live/media.go:385
if errClose := channel.Close(); errClose != nil {
log.WithError(errClose).Debug("codex live media: close unsupported downstream DataChannel")
}
return
}
session.bridge.attachDownstream(channel)
})
upstreamChannel, errChannel := upstream.CreateDataChannel(realtimeDataChannelLabel, nil)
if errChannel != nil {
_ = session.Close()
return nil, "", fmt.Errorf("create upstream DataChannel: %w", errChannel)
}
session.bridge.attachUpstream(upstreamChannel)
gatherComplete := webrtc.GatheringCompletePromise(upstream)
offer, errOffer := upstream.CreateOffer(nil)
if errOffer != nil {
_ = session.Close()
return nil, "", fmt.Errorf("create upstream WebRTC offer: %w", errOffer)
}
if errLocal := upstream.SetLocalDescription(offer); errLocal != nil {
_ = session.Close()
return nil, "", fmt.Errorf("set upstream WebRTC offer: %w", errLocal)
}
select {
case <-gatherComplete:
case <-ctx.Done():
_ = session.Close()
return nil, "", fmt.Errorf("gather upstream WebRTC candidates: %w", ctx.Err())
}
localDescription := upstream.LocalDescription()
if localDescription == nil || strings.TrimSpace(localDescription.SDP) == "" {
_ = session.Close()
return nil, "", errors.New("upstream WebRTC offer is empty")
}
session.localOffer = localDescription.SDP
return session, localDescription.SDP, nilView on GitHub (pinned to 78f0c4079e)