plandex-ai/plandex · error

token exchange failed - error reading body: %s

Error message

token exchange failed - error reading body: %s

What it means

During the Claude Max OAuth code exchange, if the token endpoint responds with a non-200 status, the code attempts to read the response body so it can build a more informative status error. This error is returned only when that io.ReadAll(resp.Body) itself fails.

Source

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

		"client_id":     claudeMaxClientId,
	})
	req, err := http.NewRequest("POST", claudeMaxTokenUrl, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("token exchange failed - error creating request: %s", 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, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		b, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("token exchange failed - error reading body: %s", err)
		}
		return nil, fmt.Errorf("token exchange failed - status: %d, body: %s", resp.StatusCode, b)
	}
	var t types.OauthResponse
	if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
		return nil, err
	}
	return &t, nil
}

func genCodeVerifier() (string, error) {
	buf := make([]byte, 32)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(buf), nil
}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Retry the token exchange — the failure was in reading the error body, so the root status is unknown and transient network issues are the likely cause.
  2. Check connectivity/proxy settings between the client and claudeMaxTokenUrl.
  3. Log resp.StatusCode even when the body read fails, so the status isn't lost.
  4. Check the Anthropic status page for an ongoing outage.

Example fix

// before
if resp.StatusCode != http.StatusOK {
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("token exchange failed - error reading body: %s", err)
	}
// after
if resp.StatusCode != http.StatusOK {
	b, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return nil, fmt.Errorf("token exchange failed - status: %d, error reading body: %w", resp.StatusCode, err)
	}
Defensive patterns

Strategy: retry

Validate before calling

client := &http.Client{Timeout: 15 * time.Second}
// pre-flight reachability
if resp, err := client.Head(claudeMaxTokenUrl); err != nil { /* warn: endpoint unreachable */ } else { resp.Body.Close() }

Type guard

func isBodyReadErr(err error) bool { return strings.Contains(err.Error(), "error reading body") }

Try / catch

for i := 0; i < 3; i++ {
	tok, err := exchangeCode(ctx, code, verifier)
	if err != nil && strings.Contains(err.Error(), "error reading body") {
		time.Sleep(time.Duration(1<<i) * time.Second)
		continue
	}
	break
}

Prevention

When it happens

Trigger: The token endpoint returned non-200 AND reading the error response body failed — network interruption mid-response, truncated/chunked response, or the server closed the connection before the body was fully received.

Common situations: Flaky network or VPN dropping the connection to the token endpoint; proxy terminating the response early; Claude API outage returning an incomplete error response.

Related errors


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