charmbracelet/crush · error
failed to decode response: %w
Error message
failed to decode response: %w
What it means
FetchCredits decodes the /v1/credits response body into a struct with a single nullable `balance` int field; a JSON syntax error, non-JSON body (HTML error page, empty body), or a type mismatch (e.g. balance as a float/string) triggers this wrapped error. Note the endpoint normally returns 200 JSON, so this indicates a malformed or unexpected payload.
Source
Thrown at internal/agent/hyper/provider.go:108
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
// Teams with hypercredit display disabled get a balance_usd field
// instead of balance, and no balance is shown for them at all.
var result struct {
Balance *int `json:"balance"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return result.Balance, nil
}
View on GitHub (pinned to 7944b8e522)
Solutions
- Capture the raw body (`curl -s $HYPER_URL/v1/credits | head -c 500`) and confirm it is valid JSON with an integer `balance` field.
- If behind a custom HYPER_URL proxy, fix it to forward Hyper's JSON response verbatim (disable HTML error pages with 200 status).
- If balance arrives as a float (e.g. 12.5), fix the server payload to emit an integer, or decode into *float64 client-side before converting.
- Check for transparent proxies/antivirus that rewrite response bodies.
Example fix
// before (server payload mismatch)
// {"balance": 12.5} -> decode into *int fails: "failed to decode response"
// after (client-side tolerant decode)
var raw struct {
Balance json.Number `json:"balance"`
}
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if raw.Balance.String() == "" {
return nil, nil // hypercredit display disabled
}
b, err := raw.Balance.Int64() Defensive patterns
Strategy: type-guard
Validate before calling
resp, err := http.Get(hyper.BaseURL() + "/v1/credits")
if err == nil {
ct := resp.Header.Get("Content-Type")
if !strings.Contains(ct, "application/json") {
return fmt.Errorf("expected JSON, got %q", ct)
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
} Type guard
func validCreditsPayload(body []byte) bool {
var v struct {
Balance *int `json:"balance"`
}
if json.Unmarshal(body, &v) != nil {
return false
}
return v.Balance == nil || *v.Balance >= 0
} Try / catch
balance, err := hyper.FetchCredits(ctx, apiKey)
if err != nil {
if strings.Contains(err.Error(), "failed to decode response") {
log.Warn("malformed credits payload from proxy; check HYPER_URL target", "err", err)
return nil, nil // treat as no balance rather than crashing UI
}
return nil, err
} Prevention
- If you proxy Hyper via HYPER_URL, ensure it forwards bodies verbatim and never emits HTML error pages with 200 status.
- Validate Content-Type is application/json before trusting the payload.
- Pin integers in the upstream payload schema (balance must be an int, not float/string).
- Smoke-test the credits endpoint after any proxy/CDN configuration change.
When it happens
Trigger: Proxy at HYPER_URL returns an HTML login/error page with status 200; response body is truncated or empty; server sends JSON with `balance` as a non-integer (e.g. 12.5 or "100"); gzip/encoding mismatch handled incorrectly by an intermediate proxy.
Common situations: Self-hosted HYPER_URL proxies that return 200 with an auth-portal HTML page; a proxy that rewrites the payload shape (balance in cents as float64); CDN interference corrupting the body.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to decode MCP prompt response: %w
- could not create request: %w
- failed to read response: %w
- failed to marshal GraphQL request: %w
- failed to create request: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/d5412f87bbc7fe34.
Report an issue: GitHub.