{"record":{"id":"644d7d06899fd5ea","repo":"mastra-ai/mastra","slug":"xai-token-response-missing-refresh-token","errorCode":null,"errorMessage":"xAI token response missing refresh_token","messagePattern":"xAI token response missing refresh_token","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/sdk/src/auth/providers/xai.ts","lineNumber":63,"sourceCode":"    throw new Error(`xAI device authorization returned a non-https verification_uri: ${raw}`);\n  }\n  return parsed.toString();\n}\n\nfunction credentialsFromTokenResponse(data: unknown, previousRefreshToken?: string): OAuthCredentials {\n  const record = (data ?? {}) as Record<string, unknown>;\n  const access = record.access_token;\n  if (typeof access !== 'string' || access.length === 0) {\n    throw new Error('xAI token response missing access_token');\n  }\n\n  // xAI may not rotate the refresh token on refresh; keep the previous one.\n  const refresh =\n    typeof record.refresh_token === 'string' && record.refresh_token.length > 0\n      ? record.refresh_token\n      : previousRefreshToken;\n  if (!refresh) {\n    throw new Error('xAI token response missing refresh_token');\n  }\n\n  const expiresIn =\n    typeof record.expires_in === 'number' && record.expires_in > 0\n      ? record.expires_in\n      : DEFAULT_TOKEN_EXPIRES_IN_SECONDS;\n\n  return {\n    access,\n    refresh,\n    expires: Date.now() + expiresIn * 1000 - REFRESH_SKEW_MS,\n  };\n}\n\n/**\n * Serializable pending state for an xAI device-code login. Safe to persist\n * (e.g. a `pending jsonb` column) so polling can span HTTP requests.\n */","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/sdk/src/auth/providers/xai.ts#L45-L81","documentation":"credentialsFromTokenResponse requires a refresh token: it accepts record.refresh_token if present, otherwise falls back to previousRefreshToken (xAI may not rotate it). If neither exists, credentials could never be renewed, so this error is thrown.","triggerScenarios":"pollXAITokenOnce or refreshXAIToken gets a response with a valid access_token but no refresh_token, while previousRefreshToken is undefined (initial device login) or also empty.","commonSituations":"xAI changes token-rotation behavior and omits refresh_token on an initial grant; the caller dropped the previous refresh token before calling refresh; scope/plan differences where the provider does not issue refresh tokens.","solutions":["Ensure the previous refresh token is passed to refreshXAIToken so the fallback works when xAI does not rotate.","Log the token response to confirm whether refresh_token was omitted by the provider.","Re-run the device login to obtain a grant that includes a refresh token.","Check xAI API docs/changelog for refresh-token issuance changes and upgrade the SDK if needed."],"exampleFix":"// before\nreturn credentialsFromTokenResponse(json); // previous refresh token lost\n// after\nreturn credentialsFromTokenResponse(json, storedRefreshToken); // keeps previous token if not rotated","handlingStrategy":"validation","validationCode":"// before refreshing, ensure you still hold the previous refresh token to pass through\nif (!previousRefreshToken && !responseIncludesRefreshToken) {\n  await runFullXaiDeviceLogin(); // refresh chain broken; re-auth\n}","typeGuard":"function hasRecoverableRefreshToken(data: unknown, previous?: string): boolean {\n  const r = (data ?? {}) as Record<string, unknown>;\n  const next = r.refresh_token;\n  return (typeof next === 'string' && next.length > 0) || (typeof previous === 'string' && previous.length > 0);\n}","tryCatchPattern":"try {\n  creds = await refreshXAIToken(stored.refresh);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('missing refresh_token')) {\n    creds = await runXaiDeviceLogin(); // no renewable token; full login\n  }\n}","preventionTips":["Always pass the previous refresh token to the refresh call so the non-rotation fallback works","Persist refresh tokens durably before dropping old credentials","Verify the provider's scope grants include offline access/refresh tokens","Watch provider changelogs for token-rotation behavior changes"],"tags":["oauth","xai","response-validation"],"backgroundTag":"missing-refresh-token","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}