{"record":{"id":"3e266793001e945a","repo":"sipeed/picoclaw","slug":"token-refresh-failed-s","errorCode":null,"errorMessage":"token refresh failed: %s","messagePattern":"token refresh failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/auth/oauth.go","lineNumber":469,"sourceCode":"\t}\n\n\ttokenURL := cfg.Issuer + \"/oauth/token\"\n\tif cfg.TokenURL != \"\" {\n\t\ttokenURL = cfg.TokenURL\n\t}\n\n\tresp, err := http.PostForm(tokenURL, data)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"refreshing token: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading token refresh response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"token refresh failed: %s\", string(body))\n\t}\n\n\trefreshed, err := parseTokenResponse(body, cred.Provider)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif refreshed.RefreshToken == \"\" {\n\t\trefreshed.RefreshToken = cred.RefreshToken\n\t}\n\tif refreshed.AccountID == \"\" {\n\t\trefreshed.AccountID = cred.AccountID\n\t}\n\tif cred.Email != \"\" && refreshed.Email == \"\" {\n\t\trefreshed.Email = cred.Email\n\t}\n\tif cred.ProjectID != \"\" && refreshed.ProjectID == \"\" {\n\t\trefreshed.ProjectID = cred.ProjectID\n\t}","sourceCodeStart":451,"sourceCodeEnd":487,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/auth/oauth.go#L451-L487","documentation":"RefreshAccessToken (pkg/auth/oauth.go:469) received a non-200 from the token endpoint and embeds the raw body. The body is an OAuth error JSON (e.g. {\"error\":\"invalid_grant\"}) but the HTTP status code is omitted from the message. invalid_grant means the refresh token is expired, revoked, or issued to a different client; invalid_client means wrong client_id/secret.","triggerScenarios":"POST to TokenURL/Issuer+'/oauth/token' with grant_type=refresh_token returns: 400 invalid_grant (expired/revoked/mismatched token), 401 invalid_client (bad client_id or client_secret), 400 invalid_request (scope not granted), 429/5xx.","commonSituations":"Refresh token expired after long inactivity or user revoked app access; rotating refresh tokens where the second use of a consumed token fails; cfg.ClientID/ClientSecret changed between environments; TokenURL pointing at the wrong provider's token endpoint; password change invalidating grants.","solutions":["Read the error field in the embedded body — it maps directly to the fix (invalid_grant → re-login; invalid_client → fix credentials)","On invalid_grant: the refresh token is dead; prompt the user to re-authenticate (LoginDeviceCode or browser flow) and replace the stored credential","Verify cfg.ClientID and cfg.ClientSecret match the provider app registration and are consistent with the token originally issued","Confirm tokenURL: cfg.TokenURL silently overrides Issuer+'/oauth/token' — a stale value hits the wrong endpoint","Log resp.StatusCode with the body; honor Retry-After on 429"],"exampleFix":"// before\nif resp.StatusCode != http.StatusOK {\n\treturn nil, fmt.Errorf(\"token refresh failed: %s\", string(body))\n}\n\n// after\nif resp.StatusCode != http.StatusOK {\n\treturn nil, fmt.Errorf(\"token refresh failed: status %d: %s\", resp.StatusCode, string(body))\n}","handlingStrategy":"try-catch","validationCode":"// Confirm the refresh setup is coherent before the call\nif cred == nil || cred.RefreshToken == \"\" {\n\treturn fmt.Errorf(\"credential lacks refresh token\")\n}\nif cfg.ClientID == \"\" {\n\treturn fmt.Errorf(\"client_id required for refresh\")\n}","typeGuard":"func isInvalidGrant(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"token refresh failed\") && strings.Contains(err.Error(), \"invalid_grant\")\n}\nfunc isInvalidClient(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"token refresh failed\") && strings.Contains(err.Error(), \"invalid_client\")\n}","tryCatchPattern":"refreshed, err := auth.RefreshAccessToken(cred, cfg)\nif err != nil {\n\tswitch {\n\tcase isInvalidGrant(err):\n\t\t// token dead: only re-login helps\n\t\trefreshed, err = reloginFlow(cfg)\n\tcase isInvalidClient(err):\n\t\treturn fmt.Errorf(\"client credentials misconfigured: %w\", err)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n}","preventionTips":["Parse the error field from embedded bodies instead of string-matching blindly","Never retry invalid_grant — go straight to re-authentication","Keep ClientID/ClientSecret consistent across environments","Log status codes with bodies; honor Retry-After for 429"],"tags":["oauth","refresh-token","http-status","invalid-grant","configuration","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}