{"record":{"id":"eb461bae4252930b","repo":"toeverything/AFFiNE","slug":"invalid-license-to-activate-eb461b","errorCode":"invalid_license_to_activate","errorMessage":"Invalid license to activate. ${reason}","messagePattern":"Invalid license to activate\\. (.+?)","errorType":"exception","errorClass":"InvalidLicenseToActivate","httpStatus":400,"severity":"warning","filePath":"packages/backend/server/src/plugins/payment/license/controller.ts","lineNumber":66,"sourceCode":"@Public()\n@Controller('/api/team/licenses')\nexport class LicenseController {\n  private readonly logger = new Logger(LicenseController.name);\n\n  constructor(\n    private readonly db: PrismaClient,\n    private readonly mutex: Mutex,\n    private readonly subscription: SubscriptionService,\n    private readonly manager: SelfhostTeamSubscriptionManager,\n    private readonly stripeProvider: StripeFactory\n  ) {}\n\n  @Post('/:license/activate')\n  async activate(@Res() res: Response, @Param('license') key: string) {\n    await using lock = await this.mutex.acquire(`license-activation:${key}`);\n\n    if (!lock) {\n      throw new InvalidLicenseToActivate({\n        reason: 'Too Many Requests',\n      });\n    }\n\n    const license = await this.db.license.findUnique({\n      where: {\n        key,\n      },\n    });\n\n    if (!license) {\n      throw new InvalidLicenseToActivate({\n        reason: 'License not found',\n      });\n    }\n\n    const subscription = await this.manager.getActiveSubscription({\n      key: license.key,","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/payment/license/controller.ts#L48-L84","documentation":"InvalidLicenseToActivate('Too Many Requests') thrown at packages/backend/server/src/plugins/payment/license/controller.ts:66 when the per-key mutex for license-activation:<key> could not be acquired. Mutex.acquire retries the locker 5 times with 100ms waits before returning undefined, and the controller immediately converts that into this error — it is a concurrency guard serializing activations of the same license key.","triggerScenarios":"Two or more concurrent POST /api/team/licenses/:license/activate calls for the same key: a client retry storm, double-clicked activate button, parallel instances activating the same license, or an activation stuck long enough (slow DB/Stripe call) that the next request exhausts the ~500ms retry budget.","commonSituations":"Self-hosted node retrying activation in a loop; admin clicks activate repeatedly while the first request is in flight; scripts firing concurrent activations during provisioning.","solutions":["Retry the activation once after a short delay — the winner usually completes within a second.","Make the client fire a single request and debounce/guard the activate button.","If persistent, check for a hung activation holding the lock (slow getActiveSubscription call to the billing API) and inspect Redis/locker health.","Never activate the same license concurrently from multiple instances."],"exampleFix":"// before\nconst activate = () => fetch(`/api/team/licenses/${key}/activate`, { method: 'POST' });\nbutton.onclick = activate; // double click -> Too Many Requests\n\n// after\nlet activating = false;\nbutton.onclick = async () => {\n  if (activating) return;\n  activating = true;\n  try { await activate(); } finally { activating = false; }\n};","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"async function activateWithRetry(key: string, attempts = 3) {\n  for (let i = 0; i < attempts; i++) {\n    const res = await fetch(`/api/team/licenses/${key}/activate`, { method: 'POST' });\n    if (res.ok) return res;\n    const body = await res.json();\n    if (body.code !== 'invalid_license_to_activate' || body.data?.reason !== 'Too Many Requests') throw body;\n    await new Promise(r => setTimeout(r, 1000 * (i + 1))); // backoff, lock frees quickly\n  }\n  throw new Error('license activation still contended');\n}","preventionTips":["Guard activate UI against double submits and serialize client retries with backoff.","Provision each license from exactly one orchestrator/instance.","Treat reason='Too Many Requests' as transient contention, not a bad key."],"tags":["license","rate-limit","mutex","concurrency","self-hosted"],"backgroundTag":"rate-limited","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}