invoke-ai/InvokeAI · error · HTTPException

str(e) (ValueError from user service delete, e.g. LastAdmini

Error message

str(e) (ValueError from user service delete, e.g. LastAdministratorError)

What it means

delete_user calls user_service.delete inside try/except ValueError; the service can raise ValueError (e.g. LastAdministratorError) if a race caused the pre-check to pass but the service still rejects the delete. The endpoint converts it to HTTP 400 with str(e) as the detail so the client gets a consistent 400 shape.

Source

Thrown at invokeai/app/api/routers/auth.py:692

    if user_id == SYSTEM_USER_ID:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=SYSTEM_USER_PROTECTED_DETAIL,
        )

    # Prevent deleting the last active admin. Same wording as the service backstop: this
    # pre-check can lose a race and let the service reject the delete instead, and one
    # endpoint should not report one condition two different ways.
    if user.is_admin and user.is_active and user_service.count_admins() <= 1:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=LAST_ADMIN_DETAIL,
        )

    try:
        user_service.delete(user_id)
    except ValueError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e

    # A deleted user must lose live access just like a deactivated one.
    ApiDependencies.invoker.services.events.emit_user_access_changed(user_id=user_id, is_admin=False, is_active=False)


@auth_router.patch("/me", response_model=UserDTO)
def update_current_user(
    request: Annotated[UserProfileUpdateRequest, Body(description="Profile fields to update")],
    current_user: CurrentUser,
    http_request: Request,
    response: Response,
) -> UserDTO:
    """Update the current user's own profile.

    To change the password, both ``current_password`` and ``new_password`` must
    be provided. The current password is verified before the change is applied.

    A password change signs out the account's *other* sessions: it bumps the

View on GitHub (pinned to 0b6a024f2f)

Solutions

  1. Read the detail string to identify the specific ValueError (e.g. LastAdministratorError) and promote another active admin, then retry
  2. Refresh user/admin list before retrying the delete to re-run the pre-check with fresh data
  3. Avoid concurrent admin mutations on the same user set

Example fix

// before
try { await api.deleteUser(id) } catch (e) { /* unhandled 400 with service message */ }
// after
try {
  await api.deleteUser(id);
} catch (e) {
  if (e.detail?.includes('administrator')) { await api.updateUser(otherId, { is_admin: true }); await api.deleteUser(id); }
  else throw e;
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await api.deleteUser(userId);
} catch (e) {
  if (e.response?.status === 400 && /administrator/i.test(e.response?.data?.detail ?? '')) {
    await api.updateUser(otherAdminId, { is_admin: true });
    await api.deleteUser(userId);
  } else throw e;
}

Prevention

When it happens

Trigger: DELETE /users/{id} where the last-admin pre-check passed but user_service.delete raises ValueError — typically a race where another admin was deactivated or the admin count changed between the check and the delete.

Common situations: Concurrent admin operations: one session deletes/deactivates the second admin while another session deletes what it believed was a non-last admin; stale UI state after another admin acted.

Related errors


AI-assisted analysis of invoke-ai/InvokeAI@0b6a024f2f (2026-08-29). Data as JSON: /api/errors/a967fe74b30f4bc8. Report an issue: GitHub.