toeverything/AFFiNE · critical · InternalServerError

internal_server_error

internal_server_error

Error message

An internal error occurred.

What it means

Thrown by CustomSetupController.createAdmin when Mutex.acquire('createFirstAdmin') fails to obtain the cross-instance lock. This indicates another process or replica is currently creating the first admin (or the mutex backend — Redis/mutex table — is unreachable/misconfigured).

Source

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

    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'
      );

      await this.sessionIssuer.issue(req, res, {
        userId: user.id,
        method: 'password',

View on GitHub (pinned to 26c515e050)

Solutions

  1. Retry the request once after a short delay — the first request likely completed initialization.
  2. Verify the mutex backend (Redis or DB) is reachable from every server replica.
  3. Add a single leader/init container responsibility so only one pod runs setup.
  4. Confirm ServerService.initialized() flips true after success so subsequent calls short-circuit at the earlier ActionForbidden guard.

Example fix

// before
await using lock = await this.mutex.acquire('createFirstAdmin');
if (!lock) throw new InternalServerError();

// after
await using lock = await this.mutex.acquire('createFirstAdmin');
if (!lock) {
  throw new InternalServerError('Setup lock unavailable — another instance may be initializing. Retry in a few seconds.');
}
Defensive patterns

Strategy: retry

Validate before calling

const initialized = await server.initialized();
if (initialized) return; // nothing to do
// only attempt setup if no other replica is initializing
const lock = await mutex.tryAcquire('createFirstAdmin');
if (!lock) return retryLater();

Type guard

// n/a

Try / catch

try {
  await createAdmin(input);
} catch (e) {
  if (e?.code === 'internal_server_error') {
    // back off and retry once; the mutex may free or setup may complete
    await sleep(2000);
    return createAdmin(input);
  }
  throw e;
}

Prevention

When it happens

Trigger: Two concurrent POST /api/setup/create-admin-user requests race past the initialized() check; multi-replica self-host deployment where two pods run setup simultaneously; the configured mutex store (Redis) is down or misconfigured so acquire returns null.

Common situations: HA deployment without a shared mutex backend; user double-clicks the setup submit; init job retries before the first run completed; Redis connection string wrong in config.

Related errors


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