can1357/oh-my-pi · error · AIError.OAuthError

${label} missing refresh_token

Error message

${label} missing refresh_token

What it means

Thrown by parseXAITokenResponse when neither the response body nor the fallback (e.g. the refresh token supplied by the caller during a refresh exchange) yields a non-empty refresh token. The library persists a refresh token so sessions can be renewed; a response without one would silently break future refreshes, so it is rejected. Note the fallback: during refresh, a response that omits refresh_token is fine if the caller passed the old one.

Source

Thrown at packages/ai/src/registry/oauth/xai-oauth.ts:342

function parseXAITokenResponse(payload: unknown, label: string, refreshTokenFallback?: string): OAuthCredentials {
	if (!isRecord(payload)) {
		throw new AIError.OAuthError(`${label} was not a JSON object`, {
			kind: "validation",
			provider: "xai",
		});
	}
	const accessToken = typeof payload.access_token === "string" ? payload.access_token : "";
	const responseRefreshToken = typeof payload.refresh_token === "string" ? payload.refresh_token : "";
	const refreshToken = responseRefreshToken || refreshTokenFallback || "";
	const expiresInSeconds = payload.expires_in;
	if (!accessToken) {
		throw new AIError.OAuthError(`${label} missing access_token`, {
			kind: "validation",
			provider: "xai",
		});
	}
	if (!refreshToken) {
		throw new AIError.OAuthError(`${label} missing refresh_token`, {
			kind: "validation",
			provider: "xai",
		});
	}
	if (typeof expiresInSeconds !== "number" || !Number.isFinite(expiresInSeconds)) {
		throw new AIError.OAuthError(`${label} missing expires_in`, {
			kind: "validation",
			provider: "xai",
		});
	}
	return {
		access: accessToken,
		refresh: refreshToken,
		expires: Date.now() + expiresInSeconds * 1000 - ACCESS_TOKEN_CLIENT_SKEW_MS,
	};
}

async function requestXAIDeviceAuthorization(

View on GitHub (pinned to 9690622007)

Solutions

  1. For refresh flows, pass the existing refresh token as refreshTokenFallback so a non-rotating response still succeeds.
  2. Re-run device login to get a fresh token set that includes a refresh_token.
  3. Inspect the raw token response — if xAI no longer returns refresh_token for your grant type, update the ai package.
  4. Check account/product entitlements (SuperGrok plan) — some tiers may issue tokens without refresh capability.

Example fix

// before: refresh call that drops the old refresh token
const creds = await refreshXAIToken(old.access, old.expires);
// after: supply the old refresh token as fallback
const creds = await refreshXAIToken(old.access, old.expires, old.refresh);
Defensive patterns

Strategy: validation

Validate before calling

// preflight before refresh: ensure you can always supply a refresh token
if (!stored.refresh) {
  throw new Error("No refresh token stored; full device login required instead of refresh");
}
// pass it as the fallback so non-rotating responses still succeed
const creds = await refreshXAIToken(stored.access, stored.expires, stored.refresh);

Type guard

function hasRefreshToken(v: unknown, fallback?: string): boolean {
  const rt = (typeof v === "object" && v !== null) ? (v as Record<string, unknown>).refresh_token : undefined;
  return (typeof rt === "string" && rt !== "") || (typeof fallback === "string" && fallback !== "");
}

Try / catch

try {
  await xaiProvider.credentials();
} catch (err) {
  if (err instanceof AIError.OAuthError && err.message.includes("missing refresh_token")) {
    logger.warn("xAI did not return a refresh token; re-running full device login", {});
    await xaiProvider.device();
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: pollXAIDeviceToken completes with a 200 body whose refresh_token is absent/empty AND no refreshTokenFallback was provided; or the credentials refresh path gets a body with no refresh_token while also having no stored refresh token to fall back on.

Common situations: xAI token endpoint omitting refresh_token on some grant types; first-time login against an endpoint variant that does not rotate refresh tokens; caller not passing the previous refresh token into the refresh exchange.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/83a7695ff59d4a3d. Report an issue: GitHub.