{"record":{"id":"4271ae6fee8d1cd2","repo":"decolua/9router","slug":"failed-to-get-user-info-error-4271ae","errorCode":null,"errorMessage":"Failed to get user info: ${error}","messagePattern":"Failed to get user info: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/services/gemini.js","lineNumber":118,"sourceCode":"    }\n\n    return projectId;\n  }\n\n  /**\n   * Get user info from Google\n   */\n  async getUserInfo(accessToken) {\n    const response = await fetch(`${this.config.userInfoUrl}?alt=json`, {\n      headers: {\n        Authorization: `Bearer ${accessToken}`,\n        Accept: \"application/json\",\n      },\n    });\n\n    if (!response.ok) {\n      const error = await response.text();\n      throw new Error(`Failed to get user info: ${error}`);\n    }\n\n    return await response.json();\n  }\n\n  /**\n   * Save Gemini CLI tokens to server\n   */\n  async saveTokens(tokens, userInfo, projectId) {\n    const { server, token, userId } = getServerCredentials();\n\n    const response = await fetch(`${server}/api/cli/providers/gemini-cli`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${token}`,\n        \"X-User-Id\": userId,\n      },","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/gemini.js#L100-L136","documentation":"GeminiCLIService.getUserInfo() (src/lib/oauth/services/gemini.js:118) fetches the authenticated user's profile from GEMINI_CONFIG.userInfoUrl (Google's userinfo endpoint, ?alt=json) with the fresh access token. A non-2xx response is wrapped into 'Failed to get user info: <body>' and thrown, aborting connect() before tokens can be saved.","triggerScenarios":"getUserInfo(accessToken) called in connect() (src/lib/oauth/services/gemini.js:220) when Google returns non-2xx — 401 if the token is invalid/scope lacks userinfo.profile or openid, 403 if the OAuth client isn't allowed the userinfo scope, or 5xx transient Google errors.","commonSituations":"User consented but deselected profile scopes; the bundled Gemini CLI OAuth client's allowed scopes changed upstream; token endpoint returned a token but the userinfo endpoint rejects it moments later (clock skew or propagation delay); transient 5xx right after login.","solutions":["Check the embedded body/status: 401 usually means scope or token problem, 5xx means retry.","Re-run connect() and approve all requested scopes (including profile/userinfo) on the Google consent screen.","Verify GEMINI_CONFIG.userInfoUrl and scopes in src/lib/oauth/constants/oauth.js still match the Gemini CLI OAuth client registration.","For 5xx, add a short backoff retry before failing.","Check system clock skew if 401 occurs immediately after a successful token exchange."],"exampleFix":"// before\nif (!response.ok) {\n  const error = await response.text();\n  throw new Error(`Failed to get user info: ${error}`);\n}\n// after\nif (!response.ok) {\n  const error = await response.text();\n  if (response.status >= 500) {\n    await new Promise((r) => setTimeout(r, 1500));\n    return this.getUserInfo(accessToken);\n  }\n  throw new Error(`Failed to get user info (HTTP ${response.status}): ${error}`);\n}","handlingStrategy":"try-catch","validationCode":"// pre-checks before the userinfo call\nif (!tokens?.access_token) throw new Error(\"No access token from exchange — cannot fetch user info\");\n// scopes granted should include profile/userinfo\nconst granted = String(tokens.scope || \"\");\nif (!/profile|openid/.test(granted)) console.warn(\"Granted scopes lack profile/userinfo — getUserInfo may 401/403:\", granted);","typeGuard":"function isTransientHttpError(status) {\n  return status === 429 || status >= 500;\n}","tryCatchPattern":"try {\n  const userInfo = await geminiService.getUserInfo(tokens.access_token);\n} catch (err) {\n  const m = /Failed to get user info: (.*)/s.exec(err.message);\n  if (m && /401|403/.test(m[1])) {\n    console.error(\"Token lacks userinfo scope — re-run connect() and approve all scopes on the consent screen.\");\n  } else if (m && /429|5\\d\\d/.test(m[1])) {\n    console.error(\"Transient Google error — retry with backoff.\");\n  } else throw err;\n}","preventionTips":["Approve every requested scope on the Google consent screen, including profile.","Call getUserInfo immediately after exchange; don't hold tokens across restarts.","Verify GEMINI_CONFIG.userInfoUrl and scopes match the current Gemini CLI OAuth client.","Keep clock skew near zero to avoid instant-401s on fresh tokens.","Add bounded retries only for 429/5xx statuses."],"tags":["network","gemini","google","oauth","userinfo","http-error"],"backgroundTag":"upstream-http-error","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}