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
- Retry the request once after a short delay — the first request likely completed initialization.
- Verify the mutex backend (Redis or DB) is reachable from every server replica.
- Add a single leader/init container responsibility so only one pod runs setup.
- 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
- Run setup from a single leader/init container in HA deployments.
- Verify the mutex backend (Redis) is reachable before invoking setup.
- Treat initialized()==true as terminal and skip the call entirely.
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
- Too many concurrent writings
- action_forbidden
- password_required
- invalid_app_config_input
- copilot_selected_sources_unavailable
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/ab08f730ea552b35.
Report an issue: GitHub.