{"record":{"id":"d5412f87bbc7fe34","repo":"charmbracelet/crush","slug":"failed-to-decode-response-w","errorCode":null,"errorMessage":"failed to decode response: %w","messagePattern":"failed to decode response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/agent/hyper/provider.go","lineNumber":108,"sourceCode":"\n\tclient := &http.Client{Timeout: 10 * time.Second}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to make request: %w\", err)\n\t}\n\tdefer resp.Body.Close() //nolint:errcheck\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t}\n\n\t// Teams with hypercredit display disabled get a balance_usd field\n\t// instead of balance, and no balance is shown for them at all.\n\tvar result struct {\n\t\tBalance *int `json:\"balance\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode response: %w\", err)\n\t}\n\n\treturn result.Balance, nil\n}\n","sourceCodeStart":90,"sourceCodeEnd":113,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/agent/hyper/provider.go#L90-L113","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (server payload mismatch)\n// {\"balance\": 12.5} -> decode into *int fails: \"failed to decode response\"\n// after (client-side tolerant decode)\nvar raw struct {\n    Balance json.Number `json:\"balance\"`\n}\nif err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {\n    return nil, fmt.Errorf(\"failed to decode response: %w\", err)\n}\nif raw.Balance.String() == \"\" {\n    return nil, nil // hypercredit display disabled\n}\nb, err := raw.Balance.Int64()","handlingStrategy":"type-guard","validationCode":"resp, err := http.Get(hyper.BaseURL() + \"/v1/credits\")\nif err == nil {\n    ct := resp.Header.Get(\"Content-Type\")\n    if !strings.Contains(ct, \"application/json\") {\n        return fmt.Errorf(\"expected JSON, got %q\", ct)\n    }\n    io.Copy(io.Discard, resp.Body)\n    resp.Body.Close()\n}","typeGuard":"func validCreditsPayload(body []byte) bool {\n    var v struct {\n        Balance *int `json:\"balance\"`\n    }\n    if json.Unmarshal(body, &v) != nil {\n        return false\n    }\n    return v.Balance == nil || *v.Balance >= 0\n}","tryCatchPattern":"balance, err := hyper.FetchCredits(ctx, apiKey)\nif err != nil {\n    if strings.Contains(err.Error(), \"failed to decode response\") {\n        log.Warn(\"malformed credits payload from proxy; check HYPER_URL target\", \"err\", err)\n        return nil, nil // treat as no balance rather than crashing UI\n    }\n    return nil, err\n}","preventionTips":["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."],"tags":["json","decoding","http","go"],"backgroundTag":"json-decode-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}