charmbracelet/crush · error
server health check failed: %s
Error message
server health check failed: %s
What it means
probeHealth performs an HTTP GET against the server's health endpoint and requires a 2xx status. Any response outside 200–299 is returned as 'server health check failed: <status>'. This surfaces servers that are listening but reporting an unhealthy or unexpected HTTP state.
Source
Thrown at internal/cmd/root.go:747
// probeHealth issues a single GET to the readiness endpoint and treats
// any 2xx response as success.
func probeHealth(ctx context.Context, h *http.Client, reqURL string, hostURL *url.URL) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return err
}
if hostURL.Scheme == "unix" || hostURL.Scheme == "npipe" {
req.Host = client.DummyHost
}
rsp, err := h.Do(req)
if err != nil {
return err
}
defer rsp.Body.Close()
_, _ = io.Copy(io.Discard, rsp.Body)
if rsp.StatusCode < 200 || rsp.StatusCode >= 300 {
return fmt.Errorf("server health check failed: %s", rsp.Status)
}
return nil
}
// restartIfStale checks whether the running server matches the current
// client version. When they differ it asks the server to stand down and,
// if it agrees, removes the stale socket so the caller can start a fresh
// server.
//
// The request is conditional and the server has the last word: it refuses
// while it is hosting anything, because BuildID derives from the
// executable's mtime, so any rebuild (including every `go run`) makes a
// second session look like an upgrade and would otherwise kill the first
// session's workspaces. Servers too old to understand the conditional
// command are left running for the same reason — the request they do
// understand is unconditional. They shut themselves down when they go
// idle, and the next client then finds no socket and spawns a current one.
//View on GitHub (pinned to 7944b8e522)
Solutions
- Read the status code in the message: 404/405 suggests route/version mismatch — make client and server versions match.
- Check server logs for the underlying 5xx cause (failed config load, DB error).
- Unset HTTP(S)_PROXY for localhost or add NO_PROXY=localhost so probes hit the server directly.
- Restart the server cleanly (kill process, remove socket and server dir) and let crush respawn it.
Defensive patterns
Strategy: retry
Validate before calling
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
rsp, err := http.DefaultClient.Do(req)
if err == nil && rsp.StatusCode >= 200 && rsp.StatusCode < 300 {
// server healthy before calling ensureServer-dependent APIs
} Try / catch
err := probeHealth(ctx, httpClient, reqURL, hostURL)
if err != nil {
var he *healthStatusError
if errors.As(err, &he) && he.StatusCode >= 500 {
// transient server state: back off and retry
time.Sleep(time.Second)
err = probeHealth(ctx, httpClient, reqURL, hostURL)
}
} Prevention
- Set NO_PROXY=localhost/127.0.0.1 so probes bypass proxies
- Keep server and client versions aligned so the health route matches
- Check server logs for 5xx causes (config load, DB init failures)
- Retry on 5xx; treat 4xx as a version/route mismatch requiring a restart
When it happens
Trigger: The health endpoint responds with 404/405 (wrong route or method), 500/503 (server-side init failure or shutting down), or a proxy in front returns an error status; the response body is discarded and only rsp.Status is reported.
Common situations: A version-mismatched server exposing a different health route; the server is mid-shutdown and returning 503; a misconfigured HTTP_PROXY intercepting localhost requests; the server's dependencies (DB, config) failed to load, causing 500s.
Related errors
- unexpected status code: %d
- request failed with status code: %d
- server health check failed: %s
- failed to refresh MCP tools: status code %d
- failed to read MCP resource: status code %d
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/a1de8993c5f97355.
Report an issue: GitHub.