I am attempting to delete users with no permissions/roles from our sites. The user's node should be kept.

This code works when the user has a few nodes. On deletion, their ownership is passed to the anonymous user.

use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use \Drupal\user\Entity\User;
function mymodule_preprocess_page(&$variables, $hook) {    
  $route_name = \Drupal::routeMatch()->getRouteName();
  $user = \Drupal::currentUser();    
  if (count($user->getRoles()) == 1 && ($route_name != "system.401" && $route_name != "user.login")) {
    $userWithoutRole = User::load($user->id());
    if ($userWithoutRole !== NULL) {
      user_cancel([], $user->id(), 'user_cancel_reassign');
    throw new UnauthorizedHttpException('insufficient permissions');

It doesn't work when the user is the author of more than 10 nodes. All the nodes are deleted when the user is deleted.

Another method is invoked, I suppose.

How can I solve this?

The same issue is present with the Auto purge users module.

2 Answers 2


user_cancel() is run in a batch. This means that either it's called in a form, or batch_process() needs to be called after it. For example, this is the code used by UserController::confirmCancel().

$edit = [
  'user_cancel_notify' => isset($account_data['cancel_notify']) ? $account_data['cancel_notify'] : $this->config('user.settings')->get('notify.status_canceled'),
user_cancel($edit, $user->id(), $account_data['cancel_method']);

// Since user_cancel() is not invoked via Form API, batch processing
// needs to be invoked manually and should redirect to the front page
// after completion.
return batch_process('<front>');

It's not necessary to delete the account, as that is already done from _user_cancel(), the batch operation callback used from user_cancel().

case 'user_cancel_reassign':
case 'user_cancel_delete':
  // Send account canceled notification if option was checked.
  if (!empty($edit['user_cancel_notify'])) {
    _user_mail_notify('status_canceled', $account);
  // …

It's also probable that hook_preprocess_page() is the wrong place to call user_cancel() as it would be called when the page is being rendered, while the batch operations require to redirect users. See batch_process(), which contains the following code.

if (($function = $batch['redirect_callback']) && function_exists($function)) {
  $function($batch_url->toString(), [
    'query' => $query_options,
else {
  return new RedirectResponse($batch_url->setAbsolute()->toString(TRUE)->getGeneratedUrl());
  • Thanks but ...Adding batch_process(); has no effect on my code. If Removing $userWithoutRole->delete(); in my code the user is not deleted. If not hook_preprocess_page(), where ? for what i am trying to achieve ?
    – Matoeil
    Apr 14, 2021 at 11:34
  • 1
    If the account isn't deleted that means that batch operations are not correctly executed. I would rather use a controller associated to a route, to be sure they are correctly executed.
    – apaderno
    Apr 14, 2021 at 11:53

In my case, I was performing mass user deletion in a Drush command. First I was calling directly the batch operation _user_cancel because calling user_cancel() was leading to perform a batch in a batch, which would not work. But the problem was that because of that I was skipping some actions of user_cancel(), like for example the call of hook_user_cancel() of any other module implementing the hook in question.

Finally I was able to call user_cancel() :

protected function cancelUsers() {
  $user_settings = \Drupal::config('user.settings');

  foreach ($this->obsoleteUser as $uid) {
    $cancel_method = $user_settings->get('cancel_method');

    $edit = [
      // Use $user_settings->get('notify.status_canceled') in
      // the following if you need to apply the user settings.
      'user_cancel_notify' => FALSE,
      'user_cancel_method' => $cancel_method,

    // user_cancel() will set batch operations itself. All is needed is a call
    // to process it.
    user_cancel($edit, $uid, $cancel_method);

And make sure in my command that I was using the batch initiated by user_cancel() :

$batch =& batch_get();

if (empty($batch)) {


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.