charmbracelet/crush · error

failed to send message to agent: status code %d

Error message

failed to send message to agent: status code %d

What it means

Fallback variant of the SendMessage status error: the server returned a non-200/202 status and decodeErrorMessage returned an empty string (empty, malformed, or non-JSON body), so only the numeric status code is reported. It indicates the server failed without a usable error body.

Source

Thrown at internal/client/proto.go:508

// for completion detection. Pass "" when the caller does not need
// to distinguish its own turn's terminal event from any concurrent
// turn on the same session (e.g. interactive TUI usage).
func (c *Client) SendMessage(ctx context.Context, id string, sessionID, runID, prompt string, attachments ...message.Attachment) error {
	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/agent", id), nil, jsonBody(proto.AgentMessage{
		SessionID:   sessionID,
		RunID:       runID,
		Prompt:      prompt,
		Attachments: proto.AttachmentsFromMessage(attachments),
	}), http.Header{"Content-Type": []string{"application/json"}})
	if err != nil {
		return fmt.Errorf("failed to send message to agent: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK && rsp.StatusCode != http.StatusAccepted {
		if msg := decodeErrorMessage(rsp.Body); msg != "" {
			return fmt.Errorf("failed to send message to agent: status code %d: %s", rsp.StatusCode, msg)
		}
		return fmt.Errorf("failed to send message to agent: status code %d", rsp.StatusCode)
	}
	return nil
}

// decodeErrorMessage attempts to decode the response body as a
// proto.Error and returns its message. It returns an empty string
// when the body is empty or cannot be decoded into a proto.Error
// with a non-empty message, letting callers fall back to a
// status-only error.
func decodeErrorMessage(body io.Reader) string {
	var e proto.Error
	if err := json.NewDecoder(body).Decode(&e); err != nil {
		return ""
	}
	return e.Message
}

// RunShellCommand runs a shell command in the workspace without triggering the agent.

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Treat the raw status as the only signal: 502/503/504 imply proxy or server availability problems — check upstream health.
  2. Capture the response body separately (via a debugging proxy or server logs) since the client discards it here.
  3. Retry with backoff for 5xx; fix auth for 401/403.
  4. Check whether an ingress/proxy sits between client and server and is masking the real response.

Example fix

// before: assuming err contains server detail
log.Fatalf("send: %v", err) // only 'status code 502'
// after: probe server health on opaque gateway errors
if strings.Contains(err.Error(), "status code 50") {
    if healthErr := probeHealth(ctx); healthErr != nil {
        return fmt.Errorf("server unhealthy: %w (send failed: %v)", healthErr, err)
    }
}
Defensive patterns

Strategy: fallback

Validate before calling

// Go: verify direct server reachability, bypassing any proxy assumption
resp, err := http.Get(serverBaseURL + "/health")
if err != nil || resp.StatusCode != 200 {
    return fmt.Errorf("server unreachable via configured base URL")
}

Type guard

// Go: detect opaque gateway statuses
func isGatewayStatus(err error) bool {
    s := err.Error()
    return strings.Contains(s, "status code 502") ||
        strings.Contains(s, "status code 503") ||
        strings.Contains(s, "status code 504")
}

Try / catch

if err := client.SendMessage(ctx, wsID, sid, runID, prompt, nil); err != nil {
    if isGatewayStatus(err) {
        return fmt.Errorf("gateway/proxy in front of the server failed: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Non-200/202 response whose body is empty, HTML (e.g. a reverse-proxy 502 page), truncated, or not decodable as proto.Error.

Common situations: Load balancer or proxy returning 502/503/504 with HTML pages; server crash before writing a JSON error; request rejected at the auth layer with an empty body.

Related errors


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