charmbracelet/crush · error
server shutdown failed: %s
Error message
server shutdown failed: %s
What it means
ShutdownServerIfIdle sends an idle-shutdown control command; this base error is created when the response status is not 200 OK. It is the plain failure case for any unexpected status (e.g. 500), while 409 and 400 are further wrapped with ErrServerBusy and ErrUnsupported sentinel errors. Note this error is returned unwrapped in the default switch branch, so callers should errors.Is-check sentinels, not string-match.
Source
Thrown at internal/client/client.go:139
// variant: a client only ever wants a server replaced, never other
// sessions killed.
//
// 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.
//View on GitHub (pinned to 7944b8e522)
Solutions
- Check the HTTP status embedded in the message to classify the failure.
- For 500s, inspect server logs for the shutdown-time failure (DB close, flush errors).
- If behind a proxy, hit the server address directly.
- If the server is busy the caller should instead see ErrServerBusy (409) — handle that sentinel separately and retry later.
Defensive patterns
Strategy: try-catch
Validate before calling
if err := client.Health(ctx); err != nil {
return fmt.Errorf("server unhealthy before shutdown attempt: %w", err)
} Type guard
func IsPlainShutdownFailure(err error) bool {
return err != nil && !errors.Is(err, ErrServerBusy) && !errors.Is(err, ErrUnsupported) &&
strings.Contains(err.Error(), "server shutdown failed")
} Try / catch
if err := client.ShutdownServerIfIdle(ctx); err != nil {
switch {
case errors.Is(err, ErrServerBusy):
return nil
case errors.Is(err, ErrUnsupported):
return client.ShutdownServer(ctx)
default:
return fmt.Errorf("unexpected shutdown status: %w", err)
}
} Prevention
- Always branch on errors.Is for ErrServerBusy/ErrUnsupported before generic handling.
- Check server logs for 500s raised during state persistence at shutdown.
- Keep client and server versions aligned.
- Probe /health before issuing control commands.
When it happens
Trigger: Calling ShutdownServerIfIdle and receiving any non-200 status other than 409/400 — most commonly a 500 from the server failing to persist state before shutdown, or a 502/503 from a proxy in front of the server.
Common situations: Server bug during shutdown (DB close failure) yielding 500; infrastructure proxy intercepting the control endpoint; server version drift where a new status code appears.
Related errors
- server is shutting down
- server is hosting live workspaces
- server busy
- server health check failed: %s
- ErrServerBusy
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/b36810b722f59e87.
Report an issue: GitHub.