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

${label} missing expires_in

Error message

${label} missing expires_in

What it means

Thrown by parseXAITokenResponse when expires_in is missing or not a finite number. The library computes the credential expiry timestamp (Date.now() + expires_in*1000, minus client skew) from this field; without it, token expiry cannot be tracked and proactive refresh would be impossible, so the response is rejected.

Source

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

	}
	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(
	fetchImpl: FetchImpl,
	signal?: AbortSignal,
): Promise<XAIDeviceAuthorization> {
	let response: Response;
	try {
		const timeoutSignal = AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS);

View on GitHub (pinned to 9690622007)

Solutions

  1. Retry the exchange — a one-off malformed response is often transient.
  2. Inspect the raw token response with curl to see how expires_in is actually encoded.
  3. If your fetchImpl or middleware coerces numbers to strings, remove that transformation.
  4. Update the ai package if xAI changed the expires_in representation.

Example fix

// before: accepting a string expiry silently
const expiresIn = String(body.expires_in);
storeExpiry(expiresIn);
// after: require a finite number before computing expiry
if (typeof body.expires_in !== "number" || !Number.isFinite(body.expires_in)) {
  throw new Error("token response missing numeric expires_in");
}
storeExpiry(Date.now() + body.expires_in * 1000);
Defensive patterns

Strategy: validation

Validate before calling

// preflight: require a numeric expires_in before computing credential expiry
const body: Record<string, unknown> = await res.json();
const expiresIn = body.expires_in;
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn)) {
  throw new Error(`xAI token response has invalid expires_in: ${JSON.stringify(expiresIn)}`);
}
const expiresAt = Date.now() + expiresIn * 1000;

Type guard

function hasNumericExpiresIn(v: unknown): v is { expires_in: number } & Record<string, unknown> {
  const n = (typeof v === "object" && v !== null) ? (v as Record<string, unknown>).expires_in : undefined;
  return typeof n === "number" && Number.isFinite(n);
}

Try / catch

try {
  await xaiProvider.credentials();
} catch (err) {
  if (err instanceof AIError.OAuthError && err.message.includes("missing expires_in")) {
    // token usable but expiry untrackable — force a fresh login so expiry is known
    await deleteStoredXAICredentials();
    return xaiProvider.credentials();
  }
  throw err;
}

Prevention

When it happens

Trigger: pollXAIDeviceToken gets a 200 body with access_token and refresh_token present but expires_in absent, null, a string (e.g. "3600"), or non-finite; or the same shape appears on the credentials refresh path.

Common situations: xAI endpoint variant returning expires_in as a string instead of a number; proxy/API-gateway stripping unknown fields; custom fetchImpl normalizing numbers to strings; xAI schema change.

Related errors


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