sipeed/picoclaw · error

reading token exchange response: %w

Error message

reading token exchange response: %w

What it means

ExchangeCodeForTokens (pkg/auth/oauth.go:562) received an HTTP response from the token endpoint but io.ReadAll failed streaming it, so a successfully issued token set was lost. Note the code is single-use: a naive retry with the same code can fail with invalid_grant.

Source

Thrown at pkg/auth/oauth.go:562

	if cfg.TokenURL != "" {
		tokenURL = cfg.TokenURL
	}

	// Determine provider name from config
	provider := "openai"
	if cfg.TokenURL != "" && strings.Contains(cfg.TokenURL, "googleapis.com") {
		provider = "google-antigravity"
	}

	resp, err := http.PostForm(tokenURL, data)
	if err != nil {
		return nil, fmt.Errorf("exchanging code for tokens: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("reading token exchange response: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("token exchange failed: %s", string(body))
	}

	return parseTokenResponse(body, provider)
}

func parseTokenResponse(body []byte, provider string) (*AuthCredential, error) {
	var tokenResp struct {
		AccessToken  string `json:"access_token"`
		RefreshToken string `json:"refresh_token"`
		ExpiresIn    int    `json:"expires_in"`
		IDToken      string `json:"id_token"`
	}
	if err := json.Unmarshal(body, &tokenResp); err != nil {
		return nil, fmt.Errorf("parsing token response: %w", err)
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Retry once with the same code verifier — if the provider says invalid_grant, the code was consumed and the whole login must restart
  2. Increase http.Client Timeout or read the body with a bounded context
  3. Check intermediary proxies for stream truncation
  4. Persist tokens immediately after a successful exchange to narrow the loss window
Defensive patterns

Strategy: retry

Type guard

func isBodyReadError(err error) bool {
	return err != nil && strings.Contains(err.Error(), "reading token exchange response")
}

Try / catch

cred, err := auth.ExchangeCodeForTokens(cfg, code, verifier, redirectURI)
if err != nil && isBodyReadError(err) {
	// code may already be consumed server-side: retry once, restart on invalid_grant
	cred, err = auth.ExchangeCodeForTokens(cfg, code, verifier, redirectURI)
	if err != nil && strings.Contains(err.Error(), "invalid_grant") {
		return reloginFlow(cfg)
	}
}

Prevention

When it happens

Trigger: Connection reset or context timeout after status 200 but before the token JSON is fully read on the /oauth/token POST.

Common situations: Flaky networks at the worst moment of the flow; aggressive client timeouts; proxy truncation. Rarer than transport or status failures but nastier because the one-time code may already be consumed.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/17dbbe7641354037. Report an issue: GitHub.