{"record":{"id":"2b2da4396b837e14","repo":"router-for-me/CLIProxyAPI","slug":"antigravity-userinfo-request-failed-status-d-2b2da4","errorCode":null,"errorMessage":"antigravity userinfo: request failed: status %d: %s","messagePattern":"antigravity userinfo: request failed: status (.+?): (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/auth/antigravity/auth.go","lineNumber":213,"sourceCode":"\tif errDo != nil {\n\t\treturn \"\", fmt.Errorf(\"antigravity userinfo: execute request: %w\", errDo)\n\t}\n\tdefer func() {\n\t\tif errClose := resp.Body.Close(); errClose != nil {\n\t\t\tlog.Errorf(\"antigravity userinfo: close body error: %v\", errClose)\n\t\t}\n\t}()\n\n\tif resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {\n\t\tbodyBytes, errRead := io.ReadAll(io.LimitReader(resp.Body, 8<<10))\n\t\tif errRead != nil {\n\t\t\treturn \"\", fmt.Errorf(\"antigravity userinfo: read response: %w\", errRead)\n\t\t}\n\t\tbody := strings.TrimSpace(string(bodyBytes))\n\t\tif body == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"antigravity userinfo: request failed: status %d\", resp.StatusCode)\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"antigravity userinfo: request failed: status %d: %s\", resp.StatusCode, body)\n\t}\n\tvar info userInfo\n\tif errDecode := json.NewDecoder(resp.Body).Decode(&info); errDecode != nil {\n\t\treturn \"\", fmt.Errorf(\"antigravity userinfo: decode response: %w\", errDecode)\n\t}\n\temail := strings.TrimSpace(info.Email)\n\tif email == \"\" {\n\t\treturn \"\", fmt.Errorf(\"antigravity userinfo: response missing email\")\n\t}\n\treturn email, nil\n}\n\n// FetchProjectID retrieves the project ID for the authenticated user via loadCodeAssist\nfunc (o *AntigravityAuth) FetchProjectID(ctx context.Context, accessToken string) (string, error) {\n\tuserAgent := o.shortUserAgent()\n\tloadReqBody := map[string]any{\n\t\t\"metadata\": antigravityLoadCodeAssistMetadata(),\n\t}","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/auth/antigravity/auth.go#L195-L231","documentation":"The userinfo endpoint returned a non-2xx status and the body text is included. Google typically returns JSON such as {\"error\":{\"code\":401,\"message\":\"Invalid Credentials\"}}, which identifies whether the token is invalid, expired, or lacks scope.","triggerScenarios":"401 Invalid Credentials from an expired/revoked token; 403 with a scope message when the email scope was not granted; 429 quota messages.","commonSituations":"Reusing persisted tokens past expiry; authorization request missing the email scope; multiple OAuth clients sharing stored tokens.","solutions":["Parse the error message in the body to pick the fix: invalid token -> re-exchange/refresh; scope -> add scopes and re-authorize; quota -> back off and retry","Always fetch userinfo immediately after a successful token exchange","Store and honor the token's expires_in so you refresh before use"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if strings.Contains(err.Error(), \"userinfo: request failed\") {\n\tswitch {\n\tcase strings.Contains(err.Error(), \"Invalid Credentials\"): // refresh token\n\tcase strings.Contains(err.Error(), \"403\"): // scope missing — re-authorize\n\t}\n}","preventionTips":["Parse the body message to choose refresh vs re-authorize","Include email scope in the authorization request"],"tags":["oauth","antigravity","http-status","error-body"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}