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

  1. Read the status code in the message: 404/405 suggests route/version mismatch — make client and server versions match.
  2. Check server logs for the underlying 5xx cause (failed config load, DB error).
  3. Unset HTTP(S)_PROXY for localhost or add NO_PROXY=localhost so probes hit the server directly.
  4. 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

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


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