decolua/9router · error · Error

Failed to get user info: ${error}

Error message

Failed to get user info: ${error}

What it means

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.

Source

Thrown at src/lib/oauth/services/gemini.js:118

    }

    return projectId;
  }

  /**
   * Get user info from Google
   */
  async getUserInfo(accessToken) {
    const response = await fetch(`${this.config.userInfoUrl}?alt=json`, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        Accept: "application/json",
      },
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Failed to get user info: ${error}`);
    }

    return await response.json();
  }

  /**
   * Save Gemini CLI tokens to server
   */
  async saveTokens(tokens, userInfo, projectId) {
    const { server, token, userId } = getServerCredentials();

    const response = await fetch(`${server}/api/cli/providers/gemini-cli`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
        "X-User-Id": userId,
      },

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Check the embedded body/status: 401 usually means scope or token problem, 5xx means retry.
  2. Re-run connect() and approve all requested scopes (including profile/userinfo) on the Google consent screen.
  3. Verify GEMINI_CONFIG.userInfoUrl and scopes in src/lib/oauth/constants/oauth.js still match the Gemini CLI OAuth client registration.
  4. For 5xx, add a short backoff retry before failing.
  5. Check system clock skew if 401 occurs immediately after a successful token exchange.

Example fix

// before
if (!response.ok) {
  const error = await response.text();
  throw new Error(`Failed to get user info: ${error}`);
}
// after
if (!response.ok) {
  const error = await response.text();
  if (response.status >= 500) {
    await new Promise((r) => setTimeout(r, 1500));
    return this.getUserInfo(accessToken);
  }
  throw new Error(`Failed to get user info (HTTP ${response.status}): ${error}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-checks before the userinfo call
if (!tokens?.access_token) throw new Error("No access token from exchange — cannot fetch user info");
// scopes granted should include profile/userinfo
const granted = String(tokens.scope || "");
if (!/profile|openid/.test(granted)) console.warn("Granted scopes lack profile/userinfo — getUserInfo may 401/403:", granted);

Type guard

function isTransientHttpError(status) {
  return status === 429 || status >= 500;
}

Try / catch

try {
  const userInfo = await geminiService.getUserInfo(tokens.access_token);
} catch (err) {
  const m = /Failed to get user info: (.*)/s.exec(err.message);
  if (m && /401|403/.test(m[1])) {
    console.error("Token lacks userinfo scope — re-run connect() and approve all scopes on the consent screen.");
  } else if (m && /429|5\d\d/.test(m[1])) {
    console.error("Transient Google error — retry with backoff.");
  } else throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/4271ae6fee8d1cd2. Report an issue: GitHub.