decolua/9router · error
xAI token refresh failed: ${err}
Error message
xAI token refresh failed: ${err} What it means
refreshAccessToken posts a refresh_token grant to xAI's token endpoint to obtain a new access token. On a non-ok response it throws 'xAI token refresh failed: <body>' with the upstream body text. A failure here usually means the refresh token is invalid, expired, or has been rotated/revoked.
Source
Thrown at src/lib/oauth/services/xai.js:171
* Refresh an access token using a refresh_token.
*/
async refreshAccessToken(refreshToken) {
const { tokenUrl } = await discoverEndpoints();
const res = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: XAI_CONFIG.clientId,
refresh_token: refreshToken,
}),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`xAI token refresh failed: ${err}`);
}
return await res.json();
}
/**
* Complete xAI OAuth flow end-to-end (CLI entrypoint).
* Returns the raw token response plus extracted email.
*/
async connect() {
const spinner = createSpinner("Starting xAI OAuth...").start();
try {
spinner.text = "Discovering xAI endpoints...";
const { authorizeUrl, tokenUrl } = await discoverEndpoints();
spinner.text = `Starting local server on port ${XAI_CONFIG.loopbackPort}...`;
let callbackParams = null;
const { port, close } = await startLocalServer((params) => {
callbackParams = params;View on GitHub (pinned to 90b52e06ff)
Solutions
- Inspect the thrown body: invalid_grant means re-authentication is required — the refresh token cannot be recovered.
- Persist the NEW refresh token immediately after each successful refresh if xAI rotates them.
- Guard against concurrent refreshes of the same account (single-flight/lock) to avoid burning rotated tokens.
- If 5xx, retry with backoff — the token may still be valid.
Example fix
// before
const t = await refreshAccessToken(stored.refreshToken); // throws when expired
// after
try {
const t = await refreshAccessToken(stored.refreshToken);
saveNewRefreshToken(t.refresh_token ?? stored.refreshToken);
} catch (e) {
if (e.message.includes('invalid_grant')) return requireReauthorization(account);
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (typeof refreshToken !== 'string' || !refreshToken) {
return requireReauthorization(account); // nothing to refresh with
} Try / catch
try {
const tokens = await refreshAccessToken(refreshToken);
if (tokens.refresh_token && tokens.refresh_token !== refreshToken) {
await persistRotatedToken(account, tokens.refresh_token); // rotation: save immediately
}
} catch (e) {
if (!e.message.startsWith('xAI token refresh failed:')) throw e;
if (e.message.includes('invalid_grant')) return requireReauthorization(account); // expired/revoked
if (/HTTP 5\d\d|fetch failed/.test(e.message)) return retryWithBackoff();
throw e;
} Prevention
- Persist rotated refresh tokens immediately after every successful refresh.
- Serialize refreshes per account (single-flight) so concurrent calls can't consume a rotated token twice.
- Detect invalid_grant and route the user to re-authorization rather than retrying forever.
- Proactively refresh before expiry using expires_in, reducing reliance on the refresh path.
When it happens
Trigger: Refresh POST returns 400 (invalid_grant — refresh token expired or revoked, or already used after rotation), 401 (invalid_client), or 5xx from xAI.
Common situations: Long-lived stored credentials finally expiring; xAI rotating refresh tokens on each use and the app reusing an old one after a concurrent refresh; user revoking the app in their xAI account settings.
Related errors
- xAI token exchange failed: ${err}
- Vertex: failed to refresh access token from ADC JSON (author
- loadCodeAssist failed: HTTP ${response.status} ${errorText.s
- onboardUser HTTP ${response.status}: ${errorText.slice(0, 20
- xAI OAuth session not found; restart the login flow and past
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/164c9ccdd3ca1290.
Report an issue: GitHub.