{"record":{"id":"65ebb9875508ab4e","repo":"mastra-ai/mastra","slug":"login-cancelled","errorCode":null,"errorMessage":"Login cancelled","messagePattern":"Login cancelled","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"mastracode/sdk/src/auth/device-code.ts","lineNumber":173,"sourceCode":"\n/**\n * Blocking poll loop for TUI flows: waits the appropriate interval between\n * polls, honors slow_down growth, aborts on the signal, and throws on\n * failure/timeout (with a clock-drift hint after slow_down responses).\n */\nexport async function pollDeviceCodeUntilComplete<T>(options: {\n  state: DeviceCodePollState;\n  pollOnce: () => Promise<DeviceCodePollOutcome<T>>;\n  signal?: AbortSignal;\n  /** Override the sleep implementation for tests. */\n  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;\n}): Promise<T> {\n  let state = options.state;\n  const sleep = options.sleep ?? abortableSleep;\n\n  while (true) {\n    if (options.signal?.aborted) {\n      throw new Error('Login cancelled');\n    }\n    if (Date.now() >= state.deadlineAt) {\n      throw new Error(timeoutMessage(state));\n    }\n\n    await sleep(nextPollDelayMs(state), options.signal);\n\n    const step = await stepDeviceCodePoll(state, options.pollOnce);\n    state = step.state;\n\n    if (step.status === 'complete') {\n      return step.result;\n    }\n    if (step.status === 'failed') {\n      throw new Error(step.error);\n    }\n  }\n}","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/sdk/src/auth/device-code.ts#L155-L191","documentation":"`pollDeviceCodeUntilComplete` implements the RFC 8628 device-authorization polling loop used by providers like Kimi Coding and xAI. Before every poll iteration it checks the caller-supplied AbortSignal; if the signal is already aborted it throws 'Login cancelled' instead of continuing to poll. This is an intentional cancellation exit, not a provider or network failure — the device flow was deliberately stopped by the caller (or an abortableSleep was interrupted mid-wait).","triggerScenarios":"Calling pollDeviceCodeUntilComplete (directly or via loginKimiCoding/loginXAI) with a signal that is already aborted at loop entry, or aborting the signal while the loop is sleeping between polls (abortableSleep rejects with the same message).","commonSituations":"User presses Ctrl+C / Escape in a TUI login prompt while waiting to authorize in the browser; a web route times out or cancels its request while a pending device-code poll is in flight; a supervisor cancels a long-running login task; reusing a stale AbortController whose signal was aborted in an earlier attempt.","solutions":["If the user cancelled, treat this as normal flow termination: clean up any pending device code and return to the login menu without retrying.","If you did not intend to cancel, inspect the AbortController lifecycle — do not pass a signal from a controller you aborted earlier; create a fresh controller per login attempt.","In server contexts, catch this error and map it to a 499/408-style response so the client's cancellation is not logged as a server error.","To wait without cancellation risk, poll with stepDeviceCodePoll instead of the blocking loop and manage timeouts yourself."],"exampleFix":"// before\nconst controller = new AbortController();\ncontroller.abort(); // aborted earlier for an unrelated reason\nconst creds = await loginKimiCoding({ signal: controller.signal }); // throws 'Login cancelled'\n// after\nconst controller = new AbortController();\ntry {\n  const creds = await loginKimiCoding({ signal: controller.signal });\n} catch (e) {\n  if (e instanceof Error && e.message === 'Login cancelled') {\n    return null; // user backed out — not an error\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"if (controller.signal.aborted) {\n  // don't start the login at all — user already cancelled\n  return null;\n}","typeGuard":"function isLoginCancelled(e: unknown): e is Error {\n  return e instanceof Error && e.message === 'Login cancelled';\n}","tryCatchPattern":"try {\n  const creds = await pollDeviceCodeUntilComplete({ state, pollOnce, signal });\n} catch (e) {\n  if (isLoginCancelled(e)) {\n    return null; // deliberate cancellation — exit quietly, no retry\n  }\n  throw e; // real timeout/provider failure — propagate\n}","preventionTips":["Check signal.aborted before initiating the login loop to fail fast.","Use one AbortController per login attempt; never reuse aborted controllers.","Catch 'Login cancelled' separately from timeouts so cancellation is not logged as an error.","For non-blocking flows, prefer stepDeviceCodePoll so cancellation is handled by your own request lifecycle.","In UIs, disable the cancel path only where cancellation would orphan server-side state."],"tags":["oauth","device-flow","cancellation","abort-signal"],"backgroundTag":"oauth-flow-cancelled","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}