plandex-ai/plandex · error

refresh failed - decode: %w

Error message

refresh failed - decode: %w

What it means

refreshCreds wraps a json.Decoder failure while parsing the token endpoint's 200 response into types.OauthResponse. A 200 was received, but the body is not the expected JSON shape — the response was truncated, is HTML from an intercepting proxy, or the API contract changed.

Source

Thrown at app/cli/lib/claude_max.go:323

	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)
	}

	return newCreds, resp.StatusCode, nil
}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Capture and log the raw body (read it fully first, then json.Unmarshal) so you can see what was actually returned.
  2. Check for a captive portal or proxy injecting HTML; bypass the proxy for the token host.
  3. Verify the client/API version pairing — update types.OauthResponse if the token endpoint schema changed.
  4. Retry on transient truncation; add Content-Length validation before decoding.

Example fix

// before
var r types.OauthResponse
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
    return nil, 0, fmt.Errorf("refresh failed - decode: %w", err)
}
// after
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
var r types.OauthResponse
if err := json.Unmarshal(b, &r); err != nil {
    return nil, 0, fmt.Errorf("refresh failed - decode: %w; body=%q", err, string(b))
}
if r.RefreshToken == "" {
    return nil, 0, fmt.Errorf("refresh failed - missing refresh_token in response")
}
Defensive patterns

Strategy: type-guard

Validate before calling

b, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
    return fmt.Errorf("cannot read 200 response body: %w", err)
}
ct := resp.Header.Get("Content-Type")
if !strings.Contains(ct, "application/json") {
    return fmt.Errorf("unexpected content type %q from token endpoint", ct)
}

Type guard

func validOauthResponse(r *types.OauthResponse) bool {
    return r != nil && r.AccessToken != "" && r.RefreshToken != "" && r.ExpiresIn > 0
}

Try / catch

var r types.OauthResponse
if err := json.Unmarshal(b, &r); err != nil {
    log.Printf("token endpoint returned non-JSON 200 body: %q", string(b))
    return fmt.Errorf("refresh failed - decode: %w", err)
}
if !validOauthResponse(&r) {
    return fmt.Errorf("refresh failed - incomplete oauth response")
}

Prevention

When it happens

Trigger: json.NewDecoder(resp.Body).Decode(&r) errors on a 200 response: empty body, HTML/text error page disguised as 200, truncated JSON, or new/renamed fields breaking types.OauthResponse unmarshaling.

Common situations: Corporate proxies or captive portals returning HTML with status 200, server bug returning an empty 200, API version change altering the OauthResponse schema, or gzip/encoding mishandling.

Related errors


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