fatedier/frp · warning

client control for run id

Error message

client control for run id [%s] is no longer current

What it means

RegisterWorkConn could not lock the current run for ctl.runID because the manager no longer has a matching entry (or the entry's run gate changed) and the manager is not closed — i.e. the control generation this work connection belongs to is gone. On error, conn ownership remains with the caller and the connection is closed there.

Solutions

  1. Confirm only one frpc uses that run ID / client identity at a time.
  2. If embedding the server, treat this error as benign: close the work conn and let the new generation request its own work connections.
  3. Check frpc logs for a concurrent reconnect; align reconnect logic so the old session's work connections are torn down before new ones are created.
Defensive patterns

Strategy: fallback

Try / catch

if err := cm.RegisterWorkConn(ctl, conn); err != nil {
    conn.Close() // caller retains ownership on error
    if !strings.Contains(err.Error(), "no longer current") { log.Warn(err) }
}

Prevention

When it happens

Trigger: A new frpc login for the same run ID replaced the old control generation (Add swapped cm.ctlsByRunID[runID]), so the old generation's work connections no longer have a current entry; or the old control closed (e.g. after heartbeat timeout) and its entry was removed. A late NewWorkConn from the old session hits this path.

Common situations: frpc reconnecting with the same run ID (server restarted, connection dropped) while old work connections are still in flight; duplicate clients reusing a run ID; races between a control being replaced and frpc fulfilling an earlier ReqWorkConn.

Related errors


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/c62d64880f9f5e25. Report an issue: GitHub.

Appendix: source

Thrown at server/control.go:316

	defer ctl.lifecycleMu.Unlock()
	if ctl.state != controlStateRunning {
		return false, nil
	}
	return true, admit(ctl.sessionCtx.LoginMsg.User, ctl.sessionCtx.WireProtocol, ctl.sessionCtx.UDPPacketCodec)
}

// RegisterWorkConn transfers conn to ctl only if ctl is still the current
// running generation. On error, ownership remains with the caller.
func (cm *ControlManager) RegisterWorkConn(ctl *Control, conn *proxy.WorkConn) error {
	entry, ok := cm.lockCurrentRun(ctl.runID, false)
	if !ok {
		cm.mu.RLock()
		closed := cm.closed
		cm.mu.RUnlock()
		if closed {
			return fmt.Errorf("control manager is closed")
		}
		return fmt.Errorf("client control for run id [%s] is no longer current", ctl.runID)
	}
	defer entry.runMu.Unlock()
	if entry.ctl != ctl || entry.id != ctl.controlID {
		return fmt.Errorf("client control for run id [%s] is no longer current", ctl.runID)
	}

	ctl.lifecycleMu.Lock()
	defer ctl.lifecycleMu.Unlock()
	if ctl.state != controlStateRunning {
		return fmt.Errorf("client control for run id [%s] is not running", ctl.runID)
	}

	select {
	case ctl.workConnCh <- conn:
		ctl.xl.Debugf("new work connection registered")
		return nil
	default:
		ctl.xl.Debugf("work connection pool is full, discarding")

View on GitHub (pinned to 6c8a8d0a97)