sipeed/picoclaw · error
token exchange failed: %s
Error message
token exchange failed: %s
What it means
ExchangeCodeForTokens (pkg/auth/oauth.go:565) got a non-200 from the token endpoint and embeds the raw body (status code not included). Standard OAuth errors appear here: invalid_grant (bad/expired/reused code, wrong redirect_uri or code_verifier), invalid_client (bad client_id/secret).
Source
Thrown at pkg/auth/oauth.go:565
// 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)
}
if tokenResp.AccessToken == "" {
return nil, fmt.Errorf("no access token in response")View on GitHub (pinned to 49183d7e8d)
Solutions
- Parse the error field in the embedded body: invalid_grant → restart login; invalid_client → fix client_id/secret; mismatch params → align redirect_uri/code_verifier
- If a network glitch interrupted a previous exchange, assume the code is consumed and restart the flow rather than retrying blindly
- Ensure redirectURI passed to ExchangeCodeForTokens exactly matches the one used in the authorize request
- Verify cfg.ClientSecret is set if the provider requires a confidential client; confirm tokenURL is the right endpoint
- Include resp.StatusCode when logging to speed up triage
Example fix
// before
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token exchange failed: %s", string(body))
}
// after
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token exchange failed: status %d: %s", resp.StatusCode, string(body))
} Defensive patterns
Strategy: try-catch
Validate before calling
// Verify exchange parameters mirror the authorize request
if code == "" || codeVerifier == "" || redirectURI == "" {
return fmt.Errorf("code, verifier, and redirect URI are all required")
}
if cfg.ClientSecret == "" && providerRequiresSecret(cfg) {
return fmt.Errorf("client_secret required by this provider")
} Type guard
func isTokenExchangeRejected(err error) bool {
return err != nil && strings.Contains(err.Error(), "token exchange failed")
} Try / catch
cred, err := auth.ExchangeCodeForTokens(cfg, code, verifier, redirectURI)
if err != nil && isTokenExchangeRejected(err) {
// body embeds the OAuth error: invalid_grant means restart login
if strings.Contains(err.Error(), "invalid_grant") {
return reloginFlow(cfg)
}
return err
} Prevention
- Pass the exact same redirect_uri used at authorization
- Never reuse an authorization code after an ambiguous failure
- Exchange the code immediately after receiving the callback
- Keep PKCE verifier/code_challenge paired from the same session
When it happens
Trigger: POST grant_type=authorization_code returns 400 invalid_grant when the code expired (~1-10 min), was already exchanged (e.g. after a mid-body read retry), redirect_uri differs from the authorize request, or the PKCE code_verifier is wrong; 401 invalid_client on credential mismatch; 429/5xx otherwise.
Common situations: User sits on the callback too long before exchange; retrying after a lost response reuses a consumed code; redirectURI not matching the one built into the authorize URL (pollDeviceCode uses {Issuer}/deviceauth/callback); client secret required but unset; TokenURL pointing at the wrong provider.
Related errors
- token refresh failed: %s
- exchanging code for tokens: %w
- reading token exchange response: %w
- error fetching models: %w
- ElevenLabs API error (status %d): %s
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/1e3e05b5145683bd.
Report an issue: GitHub.