charmbracelet/crush · warning
ErrUnsupported
ErrUnsupported
Error message
%w: %w
What it means
When ShutdownServerIfIdle receives HTTP 400 Bad Request, the server rejected the command as unknown — meaning the running server predates the idle-shutdown command (version drift). The client wraps ErrUnsupported with the base failure so errors.Is(err, ErrUnsupported) identifies legacy servers, letting callers fall back to ShutdownServer.
Source
Thrown at internal/client/client.go:146
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 {
rsp, err := c.post(ctx, "/control", nil, jsonBody(proto.ServerControl{
Command: proto.ServerControlShutdown,
}), nil)
if err != nil {View on GitHub (pinned to 7944b8e522)
Solutions
- Detect with errors.Is(err, ErrUnsupported) and fall back to the legacy ShutdownServer call.
- Kill the stale old server process and start the upgraded binary.
- Align client and server versions (restart the daemon after upgrading).
- Pin the server binary path in launchers so both sides upgrade together.
Example fix
// before
err := client.ShutdownServerIfIdle(ctx)
if err != nil { return err }
// after
if err := client.ShutdownServerIfIdle(ctx); err != nil {
if errors.Is(err, ErrUnsupported) {
return client.ShutdownServer(ctx) // legacy fallback
}
return err
} Defensive patterns
Strategy: fallback
Validate before calling
// probe whether the server supports the idle-shutdown command
if !client.SupportsIdleShutdown(ctx) {
return client.ShutdownServer(ctx)
} Type guard
func IsUnsupported(err error) bool {
return errors.Is(err, ErrUnsupported)
} Try / catch
if err := client.ShutdownServerIfIdle(ctx); err != nil {
if errors.Is(err, ErrUnsupported) {
return client.ShutdownServer(ctx) // legacy server fallback
}
return err
} Prevention
- Restart the daemon after upgrading the client so versions match.
- Keep a legacy shutdown fallback path in all shutdown logic.
- Version-check the running server at client startup.
- Kill stale old-version server processes before starting new ones.
When it happens
Trigger: Calling ShutdownServerIfIdle against an older crush server binary that only knows the legacy "shutdown" command, so its router answers 400 Bad Request for the new command.
Common situations: Upgraded client with a still-running old server process from a previous version; PATH pointing at an old binary while an old daemon lingers; mixed-version client/server setups.
Related errors
- ErrServerBusy
- server shutdown failed: %s
- server is shutting down
- server is hosting live workspaces
- server busy
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/1d9456596d2f9c67.
Report an issue: GitHub.