sipeed/picoclaw · error
parsing token response: %w
Error message
parsing token response: %w
What it means
parseTokenResponse (pkg/auth/oauth.go:579) failed to json.Unmarshal the token-endpoint body into {access_token, refresh_token, expires_in, id_token}. This is hit by RefreshAccessToken and ExchangeCodeForTokens whenever the body is not strict JSON or a field has the wrong JSON type — notably expires_in as a string ("3600") fails unmarshal into int.
Source
Thrown at pkg/auth/oauth.go:579
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")
}
var expiresAt time.Time
if tokenResp.ExpiresIn > 0 {
expiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
}
cred := &AuthCredential{
AccessToken: tokenResp.AccessToken,
RefreshToken: tokenResp.RefreshToken,
ExpiresAt: expiresAt,
Provider: provider,
AuthMethod: "oauth",
}View on GitHub (pinned to 49183d7e8d)
Solutions
- Capture the raw body and run it through a JSON validator / jq to find the offending field
- If expires_in arrives as a string, relax the struct: use json.Number or a custom flexible int type like parseFlexibleInt
- If the body is HTML, fix the network path (wrong tokenURL, intercepting proxy) — this is a transport problem masquerading as a parse problem
- Align mock/test fixtures with the provider's real schema
Example fix
// before ExpiresIn int `json:"expires_in"` // after (accept numeric strings) ExpiresIn json.Number `json:"expires_in"` // then: expiresIn, _ := tokenResp.ExpiresIn.Int64()
Defensive patterns
Strategy: type-guard
Validate before calling
// Probe the payload shape before relying on the library parse
var probe map[string]json.RawMessage
if err := json.Unmarshal(body, &probe); err != nil {
return fmt.Errorf("token endpoint returned non-JSON (len %d)", len(body))
}
if _, ok := probe["access_token"]; !ok {
return fmt.Errorf("token response missing access_token key")
} Type guard
func isTokenParseError(err error) bool {
return err != nil && strings.Contains(err.Error(), "parsing token response")
} Try / catch
cred, err := auth.ExchangeCodeForTokens(cfg, code, verifier, redirectURI)
if err != nil && isTokenParseError(err) {
// likely type mismatch (e.g. string expires_in) or HTML body; capture raw body for diagnosis
return fmt.Errorf("token payload not parseable: %w", err)
} Prevention
- Contract-test token responses including field types (expires_in as int)
- Treat HTML bodies as proxy/endpoint misconfiguration, not parse bugs
- Use json.Number for numeric fields providers type inconsistently
- Log redacted bodies on parse failure
When it happens
Trigger: Token endpoint returns 200 with: HTML/plain-text (proxy error page), a JSON error object, expires_in as a string, or a number formatted as float where an int is required — any of these fail Unmarshal and surface as 'parsing token response'.
Common situations: Provider returning string-typed expires_in (common in nonstandard OIDC implementations); Cloudflare/proxy 200-wrapped HTML; mock servers returning loosely typed fixtures; success payload nesting tokens one level deeper than expected.
Related errors
- parsing device code response: %w
- invalid integer value: %s
- failed to unmarshal response: %w
- no access token in response
- marshal result: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/ac14abd0dbe7fdd9.
Report an issue: GitHub.