{"record":{"id":"d42d5f179c59e370","repo":"AlistGo/alist","slug":"server-has-shutdown","errorCode":null,"errorMessage":"server has shutdown","messagePattern":"server has shutdown","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"server/ftp.go","lineNumber":101,"sourceCode":"\t\t\tSiteHandlers: map[string]ftpserver.SiteHandler{\n\t\t\t\t\"SIZE\": ftp.HandleSIZE,\n\t\t\t},\n\t\t},\n\t\tproxyHeader:  header,\n\t\tclients:      make(map[uint32]ftpserver.ClientContext),\n\t\tshutdownLock: sync.RWMutex{},\n\t\tisShutdown:   false,\n\t\ttlsConfig:    tlsConf,\n\t}, nil\n}\n\nfunc (d *FtpMainDriver) GetSettings() (*ftpserver.Settings, error) {\n\treturn d.settings, nil\n}\n\nfunc (d *FtpMainDriver) ClientConnected(cc ftpserver.ClientContext) (string, error) {\n\tif d.isShutdown || !d.shutdownLock.TryRLock() {\n\t\treturn \"\", errors.New(\"server has shutdown\")\n\t}\n\tdefer d.shutdownLock.RUnlock()\n\td.clients[cc.ID()] = cc\n\treturn \"AList FTP Endpoint\", nil\n}\n\nfunc (d *FtpMainDriver) ClientDisconnected(cc ftpserver.ClientContext) {\n\terr := cc.Close()\n\tif err != nil {\n\t\tutils.Log.Errorf(\"failed to close client: %v\", err)\n\t}\n\tdelete(d.clients, cc.ID())\n}\n\nfunc (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string) (ftpserver.ClientDriver, error) {\n\tvar userObj *model.User\n\tvar err error\n\tif user == \"anonymous\" || user == \"guest\" {","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/server/ftp.go#L83-L119","documentation":"Returned by FtpMainDriver.ClientConnected when a new FTP client connects after the server has begun (or completed) shutdown. The driver uses an isShutdown flag plus a write-locked RWMutex: Stop() sets the flag and takes the write lock, so new connections fail either the flag check or the TryRLock. It protects in-flight client bookkeeping from racing with the shutdown loop that closes all clients.","triggerScenarios":"Any FTP control-channel connection attempt (AUTH/USER handshake start) that arrives after ftpserver's Stop() was called, e.g. during process exit, service reload, or a graceful shutdown triggered from the admin API. Also occurs when a client reconnects while the server is draining connections.","commonSituations":"Clients with aggressive auto-reconnect polling a server that is restarting; orchestration systems (systemd/docker) sending SIGTERM while load balancers still route FTP traffic to the instance; tests that stop the FTP server and then dial it again.","solutions":["Treat it as expected during shutdown: stop/retry-loop the FTP client and wait for the server to come back before reconnecting","If hit outside a restart, check whether the service is crash-looping (inspect logs for a preceding fatal error that called Stop)","Operations: drain the load balancer before restarting the AList instance so clients are not routed to a stopping server"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"// In the FTP client: treat 'server has shutdown' as transient only while reconnecting\nif err != nil && strings.Contains(err.Error(), \"server has shutdown\") {\n    time.Sleep(backoff.Next())\n    continue // reconnect loop\n}","preventionTips":["Use bounded exponential backoff reconnect loops in long-lived FTP clients","Drain load balancers of FTP traffic before restarting the AList instance","Log the error at info level — during a restart it is expected noise, not a defect"],"tags":["ftp","lifecycle","concurrency","shutdown"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}