{"record":{"id":"f7797775bb59626e","repo":"fatedier/frp","slug":"control-is-already-closed","errorCode":null,"errorMessage":"control is already closed","messagePattern":"control is already closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/control.go","lineNumber":616,"sourceCode":"\t\tif err := recover(); err != nil {\n\t\t\txl.Errorf(\"panic error: %v\", err)\n\t\t\txl.Errorf(string(debug.Stack()))\n\t\t}\n\t}()\n\n\tvar ok bool\n\t// get a work connection from the pool\n\tselect {\n\tcase workConn, ok = <-ctl.workConnCh:\n\t\tif !ok {\n\t\t\terr = pkgerr.ErrCtlClosed\n\t\t\treturn\n\t\t}\n\t\txl.Debugf(\"get work connection from pool\")\n\tdefault:\n\t\t// no work connections available in the poll, send message to frpc to get more\n\t\tif err := ctl.msgDispatcher.Send(&msg.ReqWorkConn{}); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"control is already closed\")\n\t\t}\n\n\t\tselect {\n\t\tcase workConn, ok = <-ctl.workConnCh:\n\t\t\tif !ok {\n\t\t\t\terr = pkgerr.ErrCtlClosed\n\t\t\t\txl.Warnf(\"no work connections available, %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\tcase <-time.After(time.Duration(ctl.sessionCtx.ServerCfg.UserConnTimeout) * time.Second):\n\t\t\terr = fmt.Errorf(\"timeout trying to get work connection\")\n\t\t\txl.Warnf(\"%v\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// When we get a work connection from pool, replace it with a new one.","sourceCodeStart":598,"sourceCodeEnd":634,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/server/control.go#L598-L634","documentation":"In Control.GetWorkConn, when the pool is empty the server sends ReqWorkConn to frpc; if msgDispatcher.Send fails, the control's message stream is already broken (connection closed or dispatcher shut down), so it reports 'control is already closed'. Compare with the ErrCtlClosed sentinel returned when the workConnCh channel itself is closed.","triggerScenarios":"A user connection arrives, the work conn pool is empty, and the control connection to frpc has just died (network drop, frpc exit, server closing the control). Send(&msg.ReqWorkConn{}) errors and the wrapper converts it to this error.","commonSituations":"frpc process crashing or restarting while user traffic hits the proxy; heartbeat timeout closing the control; NAT idle timeout dropping the control TCP connection.","solutions":["Let frpc's reconnect loop re-establish the control; user connections during the gap will fail and callers should retry.","Investigate why the control dropped: check frpc logs for crashes/OOM, heartbeat timeout settings, and NAT/firewall idle timeouts.","Tune transport.heartbeatTimeout and enable transport.tcpMux to keep the control connection alive."],"exampleFix":"# frpc.toml — keep control connection healthy\n[transport]\nheartbeatTimeout = 90\ntcpMux = true","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if _, err := ctl.GetWorkConn(); err != nil {\n    if errors.Is(err, pkgerr.ErrCtlClosed) || strings.Contains(err.Error(), \"control is already closed\") {\n        // control gone: fail the user conn; frpc reconnect will restore service\n        return err\n    }\n}","preventionTips":["Keep the control connection healthy: tcpMux, heartbeatTimeout tuned above NAT idle timeouts.","Monitor frpc liveness so user traffic is not routed to a dead control."],"tags":["go","frp","workconn","connection-loss","dispatch"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}