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
- Retry once with the same code verifier — if the provider says invalid_grant, the code was consumed and the whole login must restart
- Increase http.Client Timeout or read the body with a bounded context
- Check intermediary proxies for stream truncation
- 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
- Treat a lost token-exchange body as 'possibly consumed code' — plan the recovery path
- Increase client timeout for the exchange call
- Persist credentials immediately after success
- Avoid user-visible interruption between callback and exchange
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
- reading device token response: %w
- reading token refresh response: %w
- exchanging code for tokens: %w
- error fetching models: %w
- read error response: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/17dbbe7641354037.
Report an issue: GitHub.