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

  1. Inspect the underlying error via errors.Unwrap; it pinpoints the invalid value.
  2. Delete the corrupted stored credentials and re-run the OAuth connect flow to store fresh tokens.
  3. Validate that refresh_token is valid UTF-8/ASCII when loading the credentials file.
  4. 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

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


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/5abb1939bb3c8948. Report an issue: GitHub.