{"record":{"id":"ac14abd0dbe7fdd9","repo":"sipeed/picoclaw","slug":"parsing-token-response-w","errorCode":null,"errorMessage":"parsing token response: %w","messagePattern":"parsing token response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/auth/oauth.go","lineNumber":579,"sourceCode":"\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading token exchange response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"token exchange failed: %s\", string(body))\n\t}\n\n\treturn parseTokenResponse(body, provider)\n}\n\nfunc parseTokenResponse(body []byte, provider string) (*AuthCredential, error) {\n\tvar tokenResp struct {\n\t\tAccessToken  string `json:\"access_token\"`\n\t\tRefreshToken string `json:\"refresh_token\"`\n\t\tExpiresIn    int    `json:\"expires_in\"`\n\t\tIDToken      string `json:\"id_token\"`\n\t}\n\tif err := json.Unmarshal(body, &tokenResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"parsing token response: %w\", err)\n\t}\n\n\tif tokenResp.AccessToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"no access token in response\")\n\t}\n\n\tvar expiresAt time.Time\n\tif tokenResp.ExpiresIn > 0 {\n\t\texpiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)\n\t}\n\n\tcred := &AuthCredential{\n\t\tAccessToken:  tokenResp.AccessToken,\n\t\tRefreshToken: tokenResp.RefreshToken,\n\t\tExpiresAt:    expiresAt,\n\t\tProvider:     provider,\n\t\tAuthMethod:   \"oauth\",\n\t}","sourceCodeStart":561,"sourceCodeEnd":597,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/auth/oauth.go#L561-L597","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","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"],"exampleFix":"// before\nExpiresIn int `json:\"expires_in\"`\n\n// after (accept numeric strings)\nExpiresIn json.Number `json:\"expires_in\"`\n// then: expiresIn, _ := tokenResp.ExpiresIn.Int64()","handlingStrategy":"type-guard","validationCode":"// Probe the payload shape before relying on the library parse\nvar probe map[string]json.RawMessage\nif err := json.Unmarshal(body, &probe); err != nil {\n\treturn fmt.Errorf(\"token endpoint returned non-JSON (len %d)\", len(body))\n}\nif _, ok := probe[\"access_token\"]; !ok {\n\treturn fmt.Errorf(\"token response missing access_token key\")\n}","typeGuard":"func isTokenParseError(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"parsing token response\")\n}","tryCatchPattern":"cred, err := auth.ExchangeCodeForTokens(cfg, code, verifier, redirectURI)\nif err != nil && isTokenParseError(err) {\n\t// likely type mismatch (e.g. string expires_in) or HTML body; capture raw body for diagnosis\n\treturn fmt.Errorf(\"token payload not parseable: %w\", err)\n}","preventionTips":["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"],"tags":["json","parsing","oauth","type-mismatch","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}