{"record":{"id":"ab08f730ea552b35","repo":"toeverything/AFFiNE","slug":"internal-server-error","errorCode":"internal_server_error","errorMessage":"An internal error occurred.","messagePattern":"An internal error occurred\\.","errorType":"exception","errorClass":"InternalServerError","httpStatus":500,"severity":"error","filePath":"packages/backend/server/src/core/selfhost/controller.ts","lineNumber":60,"sourceCode":"    if (await this.server.initialized()) {\n      throw new ActionForbidden('First user already created');\n    }\n\n    validators.assertValidEmail(input.email);\n\n    if (!input.password) {\n      throw new PasswordRequired();\n    }\n\n    validators.assertValidPassword(\n      input.password,\n      this.config.auth.passwordRequirements\n    );\n\n    await using lock = await this.mutex.acquire('createFirstAdmin');\n\n    if (!lock) {\n      throw new InternalServerError();\n    }\n    const user = await this.models.user.create({\n      name: input.name || undefined,\n      email: input.email,\n      password: input.password,\n      registered: true,\n    });\n\n    try {\n      await this.models.userFeature.add(\n        user.id,\n        'administrator',\n        'selfhost setup'\n      );\n\n      const issued = await this.auth.issueUser(\n        user.id,\n        this.sessionIssuer.target(req)","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/selfhost/controller.ts#L42-L78","documentation":"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.","triggerScenarios":"Two POST /create-admin-user requests racing (double click, retry, parallel setup scripts); the request that loses lock acquisition throws.","commonSituations":"Impatient double-submit on slow networks, the setup wizard retrying on timeout, or monitoring probes hitting the endpoint.","solutions":["Retry once, serially: after the winner finishes, the check turns into ActionForbidden('First user already created'), which effectively means success.","Guard the UI so setup submits exactly once."],"exampleFix":"// before\nbutton.onclick = () => api.createAdmin(form); // double click -> two concurrent posts\n\n// after\nbutton.disabled = true;\ntry { await api.createAdmin(form); }\ncatch (e) { if (isInternalServerError(e)) await checkInitialized(); }\nfinally { button.disabled = false; }","handlingStrategy":"retry","validationCode":null,"typeGuard":"const isInternalServerError = (e: unknown): e is InternalServerError =>\n  e instanceof InternalServerError;","tryCatchPattern":"try {\n  await api.createAdmin(input);\n} catch (e) {\n  if (e instanceof InternalServerError) {\n    await delay(1000);\n    if (await isInitialized()) return done(); // the concurrent winner finished\n  }\n  throw e;\n}","preventionTips":["Single-flight the setup request in the UI (disable submit while pending).","Treat a lost race as success by re-checking initialization instead of showing an error."],"tags":["selfhost","concurrency","mutex","setup"],"backgroundTag":"lock-acquisition-failed","analyzedSha":"2af30773aecd567f09b346e7b72fc69143144057","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}