router-for-me/CLIProxyAPI · warning

Codex live media session closed while configuring TCP proxy

Error message

Codex live media session closed while configuring TCP proxy

What it means

Thrown while applying an upstream WebRTC answer through a TCP proxy: installCandidateTunnels() detected that the session's done channel was already closed, meaning the media session was torn down concurrently. All freshly created TCP candidate tunnels are closed again and the error is returned from AcceptUpstreamAnswer. It is a lifecycle race between session shutdown and answer negotiation, not a protocol failure.

Source

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

	session.localOffer = localDescription.SDP
	return session, localDescription.SDP, nil
}

func (s *pionMediaSession) AcceptUpstreamAnswer(ctx context.Context, upstreamAnswer string) (string, error) {
	if s == nil || s.upstream == nil || s.downstream == nil {
		return "", errors.New("Codex live media session unavailable")
	}
	answerToApply := upstreamAnswer
	if s.proxyDialer != nil {
		rewrittenAnswer, tunnels, errProxy := prepareProxiedUpstreamAnswer(upstreamAnswer, s.localOffer, s.proxyDialer)
		if errProxy != nil {
			return "", errProxy
		}
		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)

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Treat as a benign lifecycle race: check whether the session was intentionally closed (client disconnect, shutdown) before investigating further
  2. Ensure your caller does not invoke AcceptUpstreamAnswer after Close() — serialize negotiation and teardown with the session mutex/done channel
  3. If unexpected, audit what closed the session: look for max-sessions limits in codex.live-media-relay config or watchdog timeouts
Defensive patterns

Strategy: try-catch

Validate before calling

// Go: check session liveness before applying the answer
select {
case <-session.Done():
    return errors.New("session already closed; skip answer")
default:
}
_, err := session.AcceptUpstreamAnswer(ctx, answerSDP)

Try / catch

if _, err := session.AcceptUpstreamAnswer(ctx, answerSDP); err != nil {
    if strings.Contains(err.Error(), "closed while configuring TCP proxy") {
        return nil // benign teardown race; nothing to undo
    }
    return err
}

Prevention

When it happens

Trigger: Calling AcceptUpstreamAnswer on a pionMediaSession whose Close() already ran (client disconnected, relay shutdown, or max-session eviction), while a proxyDialer is configured so prepareProxiedUpstreamAnswer built candidate tunnels.

Common situations: Client cancels the live session right as the server answer arrives; the relay evicts the session due to max-sessions pressure; a shutdown path races normal negotiation during hot config reload.

Related errors


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