toeverything/AFFiNE · error · InternalServerError

internal_server_error

internal_server_error

Error message

An internal error occurred.

What it means

InternalServerError raised at packages/backend/server/src/core/selfhost/controller.ts:59 when mutex.acquire('createFirstAdmin') returns no lock. The endpoint serializes first-admin creation with a mutex; when a concurrent request already holds it, the loser does not wait but fails here.

Solutions

  1. Retry once, serially: after the winner finishes, the check turns into ActionForbidden('First user already created'), which effectively means success.
  2. Guard the UI so setup submits exactly once.

Example fix

// before
button.onclick = () => api.createAdmin(form); // double click -> two concurrent posts

// after
button.disabled = true;
try { await api.createAdmin(form); }
catch (e) { if (isInternalServerError(e)) await checkInitialized(); }
finally { button.disabled = false; }
Defensive patterns

Strategy: retry

Type guard

const isInternalServerError = (e: unknown): e is InternalServerError =>
  e instanceof InternalServerError;

Try / catch

try {
  await api.createAdmin(input);
} catch (e) {
  if (e instanceof InternalServerError) {
    await delay(1000);
    if (await isInitialized()) return done(); // the concurrent winner finished
  }
  throw e;
}

Prevention

When it happens

Trigger: Two POST /create-admin-user requests racing (double click, retry, parallel setup scripts); the request that loses lock acquisition throws.

Common situations: Impatient double-submit on slow networks, the setup wizard retrying on timeout, or monitoring probes hitting the endpoint.

Related errors


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

Appendix: source

Thrown at packages/backend/server/src/core/selfhost/controller.ts:60

    if (await this.server.initialized()) {
      throw new ActionForbidden('First user already created');
    }

    validators.assertValidEmail(input.email);

    if (!input.password) {
      throw new PasswordRequired();
    }

    validators.assertValidPassword(
      input.password,
      this.config.auth.passwordRequirements
    );

    await using lock = await this.mutex.acquire('createFirstAdmin');

    if (!lock) {
      throw new InternalServerError();
    }
    const user = await this.models.user.create({
      name: input.name || undefined,
      email: input.email,
      password: input.password,
      registered: true,
    });

    try {
      await this.models.userFeature.add(
        user.id,
        'administrator',
        'selfhost setup'
      );

      const issued = await this.auth.issueUser(
        user.id,
        this.sessionIssuer.target(req)

View on GitHub (pinned to 2af30773ae)