{"record":{"id":"9f2eebca7c23ecdf","repo":"invoke-ai/InvokeAI","slug":"str-e-valueerror-from-user-service-update-e-g","errorCode":null,"errorMessage":"str(e) (ValueError from user service update, e.g. LastAdministratorError)","messagePattern":"str\\(e\\) \\(ValueError from user service update, e\\.g\\. LastAdministratorError\\)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"invokeai/app/api/routers/auth.py","lineNumber":617,"sourceCode":"        and before.is_active\n        and (request.is_admin is False or request.is_active is False)\n        and user_service.count_admins() <= 1\n    ):\n        raise HTTPException(\n            status_code=status.HTTP_400_BAD_REQUEST,\n            detail=LAST_ADMIN_DETAIL,\n        )\n\n    try:\n        changes = UserUpdateRequest(\n            display_name=request.display_name,\n            password=request.password,\n            is_admin=request.is_admin,\n            is_active=request.is_active,\n        )\n        updated = user_service.update(user_id, changes, strict_password_checking=config.strict_password_checking)\n    except ValueError as e:\n        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e\n\n    # Authorization state changed — notify live connections (open sockets, the\n    # session processor) so demotion/deactivation takes effect immediately\n    # instead of persisting until reconnect or token expiry. A password reset bumps\n    # the epoch without touching is_admin/is_active, and must drop the target's open\n    # sockets too, so it is part of this condition.\n    if (\n        before.is_admin != updated.is_admin\n        or before.is_active != updated.is_active\n        or before.token_epoch != updated.token_epoch\n    ):\n        ApiDependencies.invoker.services.events.emit_user_access_changed(\n            user_id=updated.user_id,\n            is_admin=updated.is_admin,\n            is_active=updated.is_active,\n            token_epoch=updated.token_epoch,\n        )\n","sourceCodeStart":599,"sourceCodeEnd":635,"githubUrl":"https://github.com/invoke-ai/InvokeAI/blob/0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06/invokeai/app/api/routers/auth.py#L599-L635","documentation":"HTTP 400 raised by update_user when `user_service.update(...)` throws ValueError (auth.py:617), detail = `str(e)`. The route pre-checks unknown ids, system-user edits, and last-admin changes, but the service is authoritative and can still reject — e.g. a race where another request already demoted the last admin (LastAdministratorError), or a weak password under strict checking.","triggerScenarios":"PATCH /users/{id} losing the last-admin race (another concurrent request demoted/deactivated the remaining admin between the pre-check and the update); password change failing strength validation with strict_password_checking; other service-level constraint violations.","commonSituations":"Two admins editing users simultaneously; automated scripts doing bulk password resets with weak passwords; TOCTOU races the friendly pre-checks cannot fully close (the comments note the pre-check can lose a race).","solutions":["Read the 400 `detail` — it is the service ValueError text (e.g. LastAdministratorError message).","For last-admin races: promote a new admin first, then retry the demotion/deactivation.","Use a password that meets strength rules or relax strict_password_checking for dev.","Serialize admin user-management operations or retry idempotent updates after re-reading state.","Re-GET the user to see current is_admin/is_active before re-patching."],"exampleFix":"// before: single blind update\nawait api.patch(`/users/${id}`, {is_active:false});\n// after: re-check state and retry once on 400\nlet r = await api.patch(`/users/${id}`, {is_active:false});\nif (r.status === 400) {\n  const u = await (await api.get(`/users/${id}`)).json();\n  if (u.is_active) throw new Error(await r.text());\n  r = await api.patch(`/users/${id}`, {is_active:false});\n}","handlingStrategy":"try-catch","validationCode":"// Re-read current state immediately before updating to shrink the race window\nconst u = await (await fetch(`/api/v1/users/${id}`)).json();\nif (u.is_admin && u.is_active) {\n  const users = await (await fetch('/api/v1/users')).json();\n  if (users.filter(x => x.is_admin && x.is_active).length <= 1) {\n    throw new Error('Promote another admin first');\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await api.patch(`/users/${id}`, changes);\n} catch (e) {\n  if (e.status === 400) {\n    // detail is the service ValueError, e.g. LastAdministratorError text\n    const fresh = await fetchUser(id);\n    if (fresh) return api.patch(`/users/${id}`, changes); // one retry after re-check\n    throw new Error(`Update rejected: ${e.detail}`);\n  }\n  throw e;\n}","preventionTips":["Serialize admin user-management operations across sessions/scripts","Re-GET the user before retrying a failed update","Promote new admins before demoting old ones to avoid last-admin races","Match strict_password_checking policy when sending password changes","Log the 400 detail verbatim — it names the exact service rejection"],"tags":["http-400","users","race-condition","validation"],"backgroundTag":"validation-failed","analyzedSha":"0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06","analyzedAt":"2026-08-29T04:46:49.967Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}