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
- Read rsp.Status in the message to identify the exact HTTP code returned.
- Confirm the server process is running and listening on the configured address.
- Retry with backoff if the server is still starting (503 during boot is common).
- 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
- Wait for the server's ready signal before first health probe.
- Bypass proxies when probing the control endpoint directly.
- Persist the actual listening port, don't assume a default.
- Use bounded exponential backoff for startup-time health checks.
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
- failed to set provider API key: %w
- failed to list workspaces: %w
- failed to create workspace: %w
- failed to get MCP auth URL: %w
- failed to get session history files: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/64d60e551dbf2e84.
Report an issue: GitHub.