toeverything/AFFiNE · warning · InvalidLicenseToActivate

invalid_license_to_activate

invalid_license_to_activate

Error message

Invalid license to activate. ${reason}

What it means

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.

Source

Thrown at packages/backend/server/src/plugins/payment/license/controller.ts:66

@Public()
@Controller('/api/team/licenses')
export class LicenseController {
  private readonly logger = new Logger(LicenseController.name);

  constructor(
    private readonly db: PrismaClient,
    private readonly mutex: Mutex,
    private readonly subscription: SubscriptionService,
    private readonly manager: SelfhostTeamSubscriptionManager,
    private readonly stripeProvider: StripeFactory
  ) {}

  @Post('/:license/activate')
  async activate(@Res() res: Response, @Param('license') key: string) {
    await using lock = await this.mutex.acquire(`license-activation:${key}`);

    if (!lock) {
      throw new InvalidLicenseToActivate({
        reason: 'Too Many Requests',
      });
    }

    const license = await this.db.license.findUnique({
      where: {
        key,
      },
    });

    if (!license) {
      throw new InvalidLicenseToActivate({
        reason: 'License not found',
      });
    }

    const subscription = await this.manager.getActiveSubscription({
      key: license.key,

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Retry the activation once after a short delay — the winner usually completes within a second.
  2. Make the client fire a single request and debounce/guard the activate button.
  3. If persistent, check for a hung activation holding the lock (slow getActiveSubscription call to the billing API) and inspect Redis/locker health.
  4. Never activate the same license concurrently from multiple instances.

Example fix

// before
const activate = () => fetch(`/api/team/licenses/${key}/activate`, { method: 'POST' });
button.onclick = activate; // double click -> Too Many Requests

// after
let activating = false;
button.onclick = async () => {
  if (activating) return;
  activating = true;
  try { await activate(); } finally { activating = false; }
};
Defensive patterns

Strategy: retry

Try / catch

async function activateWithRetry(key: string, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(`/api/team/licenses/${key}/activate`, { method: 'POST' });
    if (res.ok) return res;
    const body = await res.json();
    if (body.code !== 'invalid_license_to_activate' || body.data?.reason !== 'Too Many Requests') throw body;
    await new Promise(r => setTimeout(r, 1000 * (i + 1))); // backoff, lock frees quickly
  }
  throw new Error('license activation still contended');
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/eb461bae4252930b. Report an issue: GitHub.