charmbracelet/crush · warning
ErrServerBusy
ErrServerBusy
Error message
%w: %w
What it means
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.
Source
Thrown at internal/client/client.go:142
// A server that declines because it is in use returns an error wrapping
// [ErrServerBusy]. A server too old to know the command returns
// [ErrUnsupported]; it must be left running, since the shutdown request
// it does understand is unconditional and would take its sessions down.
func (c *Client) ShutdownServerIfIdle(ctx context.Context) error {
rsp, err := c.post(ctx, "/control", nil, jsonBody(proto.ServerControl{
Command: proto.ServerControlShutdownIfIdle,
}), nil)
if err != nil {
return err
}
defer rsp.Body.Close()
if rsp.StatusCode == http.StatusOK {
return nil
}
failure := fmt.Errorf("server shutdown failed: %s", rsp.Status)
switch rsp.StatusCode {
case http.StatusConflict:
return fmt.Errorf("%w: %w", ErrServerBusy, failure)
case http.StatusBadRequest:
// The only way a well-formed control request is rejected as bad
// is an unknown command, i.e. a server predating this one.
return fmt.Errorf("%w: %w", ErrUnsupported, failure)
}
return failure
}
// ShutdownServer sends the original, unconditional "shutdown" command.
// It exists for backward compatibility with servers that predate
// [ServerControlShutdownIfIdle]: those servers reject the idle-checked
// variant with [ErrUnsupported], so a client that has already verified
// the server is idle (e.g. via [Client.ListWorkspaces]) can fall back to
// this command to replace an old server.
//
// New servers apply the same idleness check to this command as they do
// to [ServerControlShutdownIfIdle], so it is never more dangerous.
func (c *Client) ShutdownServer(ctx context.Context) error {View on GitHub (pinned to 7944b8e522)
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.
Example fix
// before
if err := client.ShutdownServerIfIdle(ctx); err != nil { return err }
// after
if err := client.ShutdownServerIfIdle(ctx); err != nil {
if errors.Is(err, ErrServerBusy) {
return nil // workspace live; leave server running
}
return err
} Defensive patterns
Strategy: type-guard
Validate before calling
if live, _ := client.WorkspaceIsLive(ctx, wsID); live {
return nil // don't attempt idle shutdown while a workspace is active
} Type guard
func IsServerBusy(err error) bool {
return errors.Is(err, ErrServerBusy)
} Try / catch
if err := client.ShutdownServerIfIdle(ctx); err != nil {
if errors.Is(err, ErrServerBusy) {
return nil // expected: workspace still live
}
return err
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ErrUnsupported
- server is hosting live workspaces
- server shutdown failed: %s
- server is shutting down
- server busy
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/b77a20fb8f0b7d01.
Report an issue: GitHub.