{"record":{"id":"b77a20fb8f0b7d01","repo":"charmbracelet/crush","slug":"errserverbusy","errorCode":"ErrServerBusy","errorMessage":"%w: %w","messagePattern":"%w: %w","errorType":"error_code","errorClass":null,"httpStatus":409,"severity":"warning","filePath":"internal/client/client.go","lineNumber":142,"sourceCode":"// A server that declines because it is in use returns an error wrapping\n// [ErrServerBusy]. A server too old to know the command returns\n// [ErrUnsupported]; it must be left running, since the shutdown request\n// it does understand is unconditional and would take its sessions down.\nfunc (c *Client) ShutdownServerIfIdle(ctx context.Context) error {\n\trsp, err := c.post(ctx, \"/control\", nil, jsonBody(proto.ServerControl{\n\t\tCommand: proto.ServerControlShutdownIfIdle,\n\t}), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rsp.Body.Close()\n\tif rsp.StatusCode == http.StatusOK {\n\t\treturn nil\n\t}\n\tfailure := fmt.Errorf(\"server shutdown failed: %s\", rsp.Status)\n\tswitch rsp.StatusCode {\n\tcase http.StatusConflict:\n\t\treturn fmt.Errorf(\"%w: %w\", ErrServerBusy, failure)\n\tcase http.StatusBadRequest:\n\t\t// The only way a well-formed control request is rejected as bad\n\t\t// is an unknown command, i.e. a server predating this one.\n\t\treturn fmt.Errorf(\"%w: %w\", ErrUnsupported, failure)\n\t}\n\treturn failure\n}\n\n// ShutdownServer sends the original, unconditional \"shutdown\" command.\n// It exists for backward compatibility with servers that predate\n// [ServerControlShutdownIfIdle]: those servers reject the idle-checked\n// variant with [ErrUnsupported], so a client that has already verified\n// the server is idle (e.g. via [Client.ListWorkspaces]) can fall back to\n// this command to replace an old server.\n//\n// New servers apply the same idleness check to this command as they do\n// to [ServerControlShutdownIfIdle], so it is never more dangerous.\nfunc (c *Client) ShutdownServer(ctx context.Context) error {","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/client/client.go#L124-L160","documentation":"When ShutdownServerIfIdle receives HTTP 409 Conflict, the server is indicating the workspace is still live and it refuses to shut down. The client wraps ErrServerBusy with the base \"server shutdown failed\" error using Go 1.20+ multi-%w wrapping, so errors.Is(err, ErrServerBusy) works for programmatic handling.","triggerScenarios":"Calling ShutdownServerIfIdle while a workspace session is still active on the server — e.g. calling restartIfStale while a TUI session holds the workspace, or a test intentionally keeping a workspace live.","commonSituations":"A crashed client left a workspace marked live server-side; multiple client processes sharing one server; calling shutdown from a watchdog while a user session is legitimately open.","solutions":["Detect the condition with errors.Is(err, ErrServerBusy) and skip the restart instead of forcing shutdown.","Ensure the owning client closed its session (or call ShutdownServer for the unconditional legacy path deliberately).","Check for orphaned/stale workspace sessions server-side if you believe nothing is live.","Avoid racing: only run restartIfStale when no other client process is running."],"exampleFix":"// before\nif err := client.ShutdownServerIfIdle(ctx); err != nil { return err }\n// after\nif err := client.ShutdownServerIfIdle(ctx); err != nil {\n    if errors.Is(err, ErrServerBusy) {\n        return nil // workspace live; leave server running\n    }\n    return err\n}","handlingStrategy":"type-guard","validationCode":"if live, _ := client.WorkspaceIsLive(ctx, wsID); live {\n    return nil // don't attempt idle shutdown while a workspace is active\n}","typeGuard":"func IsServerBusy(err error) bool {\n    return errors.Is(err, ErrServerBusy)\n}","tryCatchPattern":"if err := client.ShutdownServerIfIdle(ctx); err != nil {\n    if errors.Is(err, ErrServerBusy) {\n        return nil // expected: workspace still live\n    }\n    return err\n}","preventionTips":["Always errors.Is(err, ErrServerBusy) — never string-match on 409.","Close all client sessions before watchdog-driven restarts.","Detect and clean orphaned live-workspace state server-side.","Serialize shutdown attempts across concurrent client processes."],"tags":["http","shutdown","concurrency","sentinel-error"],"backgroundTag":"server-busy-conflict","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}