plandex-ai/plandex · error
refresh failed - read body: %w
Error message
refresh failed - read body: %w
What it means
refreshCreds wraps an io.ReadAll failure on the response body returned with a non-200 status from the token endpoint. Because the server already signaled an error status, this usually means the connection was truncated or reset mid-body, so the error text cannot be captured.
Source
Thrown at app/cli/lib/claude_max.go:316
}
req, err := http.NewRequest("POST", claudeMaxTokenUrl, bytes.NewReader(body))
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("anthropic-beta", shared.AnthropicClaudeMaxBetaHeader)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - http: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - read body: %w", err)
}
return nil, resp.StatusCode, fmt.Errorf("refresh failed - status %d: %s", resp.StatusCode, b)
}
var r types.OauthResponse
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, 0, fmt.Errorf("refresh failed - decode: %w", err)
}
newCreds := &types.OauthCreds{
OauthResponse: r,
ExpiresAt: time.Now().Add(time.Duration(r.ExpiresIn) * time.Second),
}
// persist updated creds
accountCreds.ClaudeMax = newCreds
if err := SetAccountCredentials(accountCreds); err != nil {
return nil, 0, fmt.Errorf("refresh failed - save: %w", err)View on GitHub (pinned to e2d772072e)
Solutions
- Retry the refresh; truncation is typically transient.
- Log the status code alongside this error so the actual failure (401/403/5xx) is visible even when the body is lost.
- Check for proxies/load balancers with small response timeouts between the client and the token endpoint.
- Fall back to io.ReadAtLeast/io.LimitReader if the body is expected to be small and partially readable.
Example fix
// before
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - read body: %w", err)
}
// after
b, err := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
if err != nil {
return nil, resp.StatusCode, fmt.Errorf("refresh failed - read body (status %d): %w", resp.StatusCode, err)
} Defensive patterns
Strategy: retry
Try / catch
b, err := io.ReadAll(resp.Body)
if err != nil {
if resp.StatusCode >= 400 {
return fmt.Errorf("refresh failed (status %d) and body unread: %w", resp.StatusCode, err)
}
return retryWithBackoff(refreshCreds, 3) // transient truncation
} Prevention
- Always log the status code when body reads fail so the real error is visible.
- Retry truncated reads — they are usually transient network/proxy issues.
- Audit intermediate proxies/load balancers for response timeouts.
When it happens
Trigger: resp.StatusCode != 200 and io.ReadAll(resp.Body) returns an error — the server closed the connection early, a proxy/timeout cut the body off, or the response stream is otherwise unreadable.
Common situations: Gateway or proxy (403/502 responses) that resets the connection while sending an HTML error page, aggressive server-side timeouts on slow links, or TLS interception appliances killing the stream.
Related errors
- token exchange failed - error reading body: %s
- refresh failed - http: %w
- failed to update context: %v
- failed to fetch the URL %s: %v
- failed to download the update: %w
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/ffea92c3e3cc9fe8.
Report an issue: GitHub.