charmbracelet/crush · error

server health check failed: %s

Error message

server health check failed: %s

What it means

Client.Health performs a GET /health against the server; this error is returned when the HTTP response status is anything other than 200 OK. The message carries rsp.Status (e.g. "503 Service Unavailable"), so it reflects the server's own reported unhealthiness rather than a transport failure (transport errors are returned directly by c.get).

Source

Thrown at internal/client/client.go:100

	if err != nil {
		return nil, err
	}
	defer rsp.Body.Close()
	if err := json.NewDecoder(rsp.Body).Decode(&cfg); err != nil {
		return nil, err
	}
	return &cfg, nil
}

// Health checks the server's health status.
func (c *Client) Health(ctx context.Context) error {
	rsp, err := c.get(ctx, "/health", nil, nil)
	if err != nil {
		return err
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return fmt.Errorf("server health check failed: %s", rsp.Status)
	}
	return nil
}

// VersionInfo retrieves the server's version information.
func (c *Client) VersionInfo(ctx context.Context) (*proto.VersionInfo, error) {
	var vi proto.VersionInfo
	rsp, err := c.get(ctx, "version", nil, nil)
	if err != nil {
		return nil, err
	}
	defer rsp.Body.Close()
	if err := json.NewDecoder(rsp.Body).Decode(&vi); err != nil {
		return nil, err
	}
	return &vi, nil
}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Read rsp.Status in the message to identify the exact HTTP code returned.
  2. Confirm the server process is running and listening on the configured address.
  3. Retry with backoff if the server is still starting (503 during boot is common).
  4. Verify no proxy intercepts the request; query the server address directly.

Example fix

// before
if err := client.Health(ctx); err != nil { return err }
// after
// tolerate transient unavailability during startup
var lastErr error
for i := 0; i < 5; i++ {
    if err := client.Health(ctx); err == nil {
        lastErr = nil
        break
    } else {
        lastErr = err
        time.Sleep(time.Duration(1<<i) * 200 * time.Millisecond)
    }
}
if lastErr != nil { return lastErr }
Defensive patterns

Strategy: retry

Validate before calling

conn, err := net.DialTimeout("tcp", serverAddr, 2*time.Second)
if err != nil {
    return fmt.Errorf("server not reachable at %s: %w", serverAddr, err)
}
conn.Close()

Type guard

func IsUnhealthyStatus(err error) bool {
    return err != nil && strings.Contains(err.Error(), "server health check failed")
}

Try / catch

if err := client.Health(ctx); err != nil {
    if IsUnhealthyStatus(err) {
        // retry with exponential backoff; 503 during startup is transient
        return retryHealth(ctx, client, 5)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Health(ctx) and receiving a non-200 status: server is starting up or shutting down, an intermediate proxy returns 502/503/504, wrong port hitting a different service, or authentication gateway rejecting the request.

Common situations: Client started before server finished listening behind a proxy; stale port from a previous run; reverse proxy with health checks disabled; server process crashed but socket still answered via supervisor.

Related errors


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