{"record":{"id":"0e0b0f2a6a85b0db","repo":"CherryHQ/cherry-studio","slug":"registration-polling-aborted","errorCode":null,"errorMessage":"Registration polling aborted","messagePattern":"Registration polling aborted","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"info","filePath":"src/main/ai/channels/adapters/feishu/FeishuAppRegistration.ts","lineNumber":96,"sourceCode":"    deviceCode,\n    verificationUri,\n    interval: (res.interval as number) ?? 5,\n    expiresIn: (res.expires_in as number) ?? 600\n  }\n}\n\nexport async function registrationPoll(\n  domain: FeishuDomain,\n  deviceCode: string,\n  options: { interval: number; expiresIn: number; signal?: AbortSignal }\n): Promise<RegistrationResult> {\n  const baseUrl = BASE_URLS[domain]\n  const deadline = Date.now() + options.expiresIn * 1000\n  let interval = options.interval * 1000\n\n  while (Date.now() < deadline) {\n    if (options.signal?.aborted) {\n      throw new Error('Registration polling aborted')\n    }\n\n    await delay(interval, { signal: options.signal })\n\n    const res = await postRegistration(baseUrl, {\n      action: 'poll',\n      device_code: deviceCode\n    })\n\n    // Success: got credentials\n    if (res.client_id && res.client_secret) {\n      const userInfo = res.user_info as Record<string, string> | undefined\n      logger.info('Feishu app registration succeeded')\n      return {\n        appId: res.client_id as string,\n        appSecret: res.client_secret as string,\n        openId: userInfo?.open_id\n      }","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/feishu/FeishuAppRegistration.ts#L78-L114","documentation":"Thrown by registrationPoll() when the passed AbortSignal is aborted before the deadline. The poll loop checks options.signal?.aborted at the top of each iteration and throws this constant message so callers can distinguish a user/system cancellation from a real failure.","triggerScenarios":"Caller passes an AbortSignal that fires (abort()) while registrationPoll is looping between deadline checks; the next iteration observes signal.aborted and throws.","commonSituations":"User cancelled the QR-code registration flow from the UI, the app is shutting down and aborted in-flight polls, or a navigation/unmount aborted the controller.","solutions":["Treat this error as benign cancellation, not a fault — catch it and surface 'cancelled' to the UI.","Only abort the signal on explicit user cancellation or teardown; do not reuse general-purpose signals.","Ensure abort() is called exactly once when the registration screen unmounts."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Only abort the signal on real cancellation/teardown.\nconst controller = new AbortController()\nonUserCancel(() => controller.abort())\nonTeardown(() => controller.abort())","typeGuard":"function isRegistrationAborted(e: unknown): e is Error {\n  return e instanceof Error && e.message === 'Registration polling aborted'\n}","tryCatchPattern":"try {\n  await registrationPoll(domain, deviceCode, { interval, expiresIn, signal: controller.signal })\n} catch (e) {\n  if (isRegistrationAborted(e)) { logger.info('registration cancelled by user/teardown'); return }\n  throw e\n}","preventionTips":["Treat 'Registration polling aborted' as benign cancellation, not a fault.","Abort exactly once on unmount/teardown; do not reuse the signal.","Distinguish cancellation from real errors in the UI."],"tags":["feishu","registration","abort","cancellation","lifecycle"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}