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