decolua/9router · error · Error
Failed to get user info: ${error}
Error message
Failed to get user info: ${error} What it means
AntigravityService.getUserInfo calls the Google userinfo endpoint with the bearer access token and throws this on non-2xx, including the response body. It means the token was rejected or the userinfo endpoint failed — most often 401 from an expired or insufficiently-scoped access token.
Source
Thrown at src/lib/oauth/services/antigravity.js:74
}
return await response.json();
}
/**
* 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();
}
/**
* Get common headers for Antigravity API calls
*/
getApiHeaders(accessToken) {
return {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": this.config.loadCodeAssistUserAgent,
};
}
/**
* Get metadata object for loadCodeAssist / onboardUser API calls.View on GitHub (pinned to 90b52e06ff)
Solutions
- Refresh the access token (or re-run OAuth) before calling getUserInfo — 401 means expired/invalid token
- Verify config.scopes includes userinfo.email/userinfo.profile
- Check the exchange response actually contained access_token before calling getUserInfo
- If 5xx, retry after a short delay
- Validate the stored token is a non-empty JWT-shaped string before use
Example fix
// before
const info = await svc.getUserInfo(accessToken);
// after
const guard = (t) => typeof t === 'string' && t.split('.').length === 3;
if (!guard(accessToken)) throw new Error('No valid access token — re-run OAuth');
const info = await svc.getUserInfo(accessToken); // refresh token first if >1h old Defensive patterns
Strategy: validation
Validate before calling
// pre-flight: only call userinfo with a plausible, fresh bearer token
const jwtShaped = (t) => typeof t === 'string' && t.split('.').length === 3;
const tokenAgeOk = (obtainedAt) => Date.now() - obtainedAt < 55 * 60 * 1000; // Google tokens ~1h
if (!jwtShaped(accessToken) || !tokenAgeOk(tokenObtainedAt)) {
accessToken = await refreshAccessToken(refreshToken); // or re-run OAuth
} Type guard
const isUserInfoError = (e) => e instanceof Error && e.message.startsWith('Failed to get user info:');
const isAuthError = (e) => isUserInfoError(e) && /\b40[13]\b|Unauthorized|Forbidden/.test(e.message); Try / catch
try { info = await svc.getUserInfo(accessToken); }
catch (e) {
if (isAuthError(e)) { accessToken = await refresh(); info = await svc.getUserInfo(accessToken); }
else throw e;
} Prevention
- Refresh the access token before any call made long after the OAuth exchange
- Include userinfo.email/userinfo.profile scopes in config.scopes
- Confirm the exchange response contained access_token before storing/using it
- Retry only on 5xx; 401/403 always require a token refresh or re-auth
When it happens
Trigger: Access token expired (Google tokens last ~1h) and used after expiry; token lacks the userinfo.email scope; the token endpoint returned no access_token but the flow continued; Google userinfo endpoint returning 4xx/5xx; network/proxy failure.
Common situations: Fetching the profile long after the OAuth exchange with a never-refreshed token; scopes in ANTIGRAVITY_CONFIG reduced so the email scope is missing; corrupted token stored from a partial exchange response.
Related errors
- Failed to get user info: ${error}
- access_token is required
- `Token exchange failed: ${error}`
- Token exchange failed: ${error}
- Failed to load code assist: ${errorText}
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/2e5f92eb289fce88.
Report an issue: GitHub.