{"record":{"id":"92a89fdbbc6616b0","repo":"sipeed/picoclaw","slug":"usage-request-failed-d-s","errorCode":null,"errorMessage":"usage request failed (%d): %s","messagePattern":"usage request failed \\((.+?)\\): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/auth/anthropic_usage.go","lineNumber":52,"sourceCode":"\treq.Header.Set(\"Anthropic-Beta\", anthropicBetaHeader)\n\n\tclient := &http.Client{Timeout: 10 * time.Second}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading usage response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tif resp.StatusCode == http.StatusForbidden {\n\t\t\treturn nil, fmt.Errorf(\"insufficient scope: usage endpoint requires oauth scope\")\n\t\t}\n\t\treturn nil, fmt.Errorf(\"usage request failed (%d): %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar result struct {\n\t\tFiveHour struct {\n\t\t\tUtilization float64 `json:\"utilization\"`\n\t\t} `json:\"five_hour\"`\n\t\tSevenDay struct {\n\t\t\tUtilization float64 `json:\"utilization\"`\n\t\t} `json:\"seven_day\"`\n\t}\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing usage response: %w\", err)\n\t}\n\n\treturn &AnthropicUsage{\n\t\tFiveHourUtilization: result.FiveHour.Utilization,\n\t\tSevenDayUtilization: result.SevenDay.Utilization,\n\t}, nil","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/auth/anthropic_usage.go#L34-L70","documentation":"The usage request returned a non-200 status other than 403 (anthropic_usage.go:52); the status code and raw response body are embedded in the message. Typical codes: 401 (expired/invalid token), 429 (rate limited), 500/503 (upstream incidents). The body text is the fastest way to tell which.","triggerScenarios":"GET to the usage endpoint with an expired access token (401); hammering the endpoint beyond its rate limit (429); Anthropic-side incident returning 5xx with an error JSON body; wrong region/auth header producing 400.","commonSituations":"Long-lived process holding a token past expiry without refresh; tight polling loops (e.g. every few seconds) triggering 429; API incidents; clock skew breaking token validity windows.","solutions":["Parse the (%d) status from the message: 401 -> refresh/re-login first; 429 -> back off; 5xx -> retry later","For 401, refresh the credential and retry once","For 429, reduce polling frequency and add exponential backoff with jitter","Check status.anthropic.com for incidents when 5xx bodies appear"],"exampleFix":"// before: fixed-interval usage polling\nfor range time.Tick(5 * time.Second) { fetchUsage(token) }\n\n// after: backoff on 429/5xx, refresh on 401, halt on 403\nswitch {\ncase strings.Contains(msg, \"(401)\"): refreshAndRetry()\ncase strings.Contains(msg, \"(429)\"): time.Sleep(backoff())\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"usage, err := fetchUsage(ctx, token)\nif err != nil && strings.Contains(err.Error(), \"usage request failed\") {\n    msg := err.Error()\n    switch {\n    case strings.Contains(msg, \"(401)\"): refreshCredential(); return retryLater\n    case strings.Contains(msg, \"(429)\"), strings.Contains(msg, \"(5\"): return retryWithBackoff\n    default: return err // 4xx other than above: non-retryable\n    }\n}","preventionTips":["Parse the embedded (%d) status before deciding to retry","Refresh tokens on 401, back off on 429/5xx, halt on other 4xx","Rate-limit usage polling to avoid 429s in the first place"],"tags":["http","auth","rate-limit","retry","anthropic"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}