plandex-ai/plandex · error
refresh failed - marshal: %w
Error message
refresh failed - marshal: %w
What it means
In refreshCreds, the refresh request body (grant_type=refresh_token, refresh_token, client_id) is built with json.Marshal. This error is returned if marshaling that map fails. The %w wrap preserves the underlying error for errors.Is/As inspection.
Source
Thrown at app/cli/lib/claude_max.go:297
func needsRefresh(creds *types.OauthCreds) bool {
// refresh an hour early so we can make multiple calls before it expires
return time.Now().After(creds.ExpiresAt.Add(-1 * time.Hour))
}
func refreshCreds(accountCreds *types.AccountCredentials) (*types.OauthCreds, int, error) {
creds := accountCreds.ClaudeMax
if creds == nil {
return nil, 0, fmt.Errorf("no stored Claude credentials")
}
body, err := json.Marshal(map[string]any{
"grant_type": "refresh_token",
"refresh_token": creds.RefreshToken,
"client_id": claudeMaxClientId,
})
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - marshal: %w", err)
}
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 {View on GitHub (pinned to e2d772072e)
Solutions
- Inspect the underlying error via errors.Unwrap; it pinpoints the invalid value.
- Delete the corrupted stored credentials and re-run the OAuth connect flow to store fresh tokens.
- Validate that refresh_token is valid UTF-8/ASCII when loading the credentials file.
- Avoid hand-editing or binary-transforming the credentials store.
Example fix
// before
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - marshal: %w", err)
}
// after
if creds.RefreshToken == "" || !utf8.ValidString(creds.RefreshToken) {
return nil, 0, fmt.Errorf("refresh failed - stored refresh token is invalid; re-run OAuth connect")
}
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - marshal: %w", err)
} Defensive patterns
Strategy: validation
Validate before calling
func validRefreshToken(s string) bool { return s != "" && utf8.ValidString(s) }
// before refresh:
// if !validRefreshToken(accountCreds.ClaudeMax.RefreshToken) { /* re-run OAuth connect */ } Type guard
func refreshTokenUsable(c *types.OauthCreds) bool { return c != nil && c.RefreshToken != "" && utf8.ValidString(c.RefreshToken) } Try / catch
out, n, err := refreshCreds(accountCreds)
if err != nil {
var marshaled bool
if errors.As(err, new(*json.UnsupportedTypeError)) || errors.As(err, new(*json.UnsupportedValueError)) {
marshaled = true
}
if marshaled { /* credentials corrupted: re-auth */ }
} Prevention
- Load the credentials file through a strict JSON schema parser to catch corruption early.
- Never hand-edit or binary-transform the credentials store.
- Validate refresh_token is non-empty, valid UTF-8 on load.
- Re-run the OAuth connect flow to recover from any stored-credential corruption.
When it happens
Trigger: json.Marshal of the refresh-token request map fails — in practice only if creds.RefreshToken contains bytes invalid for JSON encoding (e.g. invalid UTF-8 read from a corrupted credentials store).
Common situations: A corrupted or hand-edited credentials file put non-UTF-8 bytes into refresh_token; a broken secret store/decoder produced invalid string data.
Related errors
- no stored Claude credentials
- token exchange failed - error creating request: %s
- token exchange failed - error reading body: %s
- token exchange failed - status: %d, body: %s
- refresh failed - decode: %w
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/5abb1939bb3c8948.
Report an issue: GitHub.