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

  1. Detect with errors.Is(err, ErrUnsupported) and fall back to the legacy ShutdownServer call.
  2. Kill the stale old server process and start the upgraded binary.
  3. Align client and server versions (restart the daemon after upgrading).
  4. 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

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


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/1d9456596d2f9c67. Report an issue: GitHub.