How to make unpublished nodes "404 Not Found" instead of "Access Denied" for those without access

Last updated on 09/04/2021

Drupal 8+ example

Starting with Drupal 8, we can use Symfony's Event subscriber to achieve the desired result

First, we need to define the EventSubscriber in our services file


my_module.services.yml
services:
  my_module.exception_listener:
    class: Drupal\my_module\EventSubscriber\AccessDeniedAsNotFoundEventSubscriber
    arguments: ['@router.admin_context']
    tags:
      - { name: event_subscriber }

Then, the EventSubscriber itself


/my_module/src/EventSubscriber/AccessDeniedAsNotFoundEventSubscriber.php
<?php
 
namespace Drupal\my_module\EventSubscriber;
 
use Drupal\Core\Routing\AdminContext;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
 
/**
 * Provides a subscriber to set the properly exception.
 */
class AccessDeniedAsNotFoundEventSubscriber implements EventSubscriberInterface {
 
 
  /**
   * The admin context.
   *
   * @var \Drupal\Core\Routing\AdminContext
   */
  protected $adminContext;
 
  /**
   * {@inheritdoc}
   */
  public function __construct(AdminContext $admin_context) {
    $this->adminContext = $admin_context;
  }
 
  /**
   * Registers the methods in this class that should be listeners.
   *
   * @return array
   *   An array of event listener definitions.
   */
  public static function getSubscribedEvents() {
    $events[KernelEvents::EXCEPTION][] = ['onAccessDeniedException', 50];
    return $events;
  }
 
  /**
   * Set the properly exception for event.
   *
   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
   *   The response for exception event.
   */
  public function onAccessDeniedException(GetResponseForExceptionEvent $event) {
    if ($event->getException() instanceof AccessDeniedHttpException && !$this->adminContext->isAdminRoute()) {
      $event->setException(new NotFoundHttpException());
    }
  }
 
}


Drupal 7 example

/**
 * Implements hook_page_delivery_callback_alter().
 */
function mymodule_page_delivery_callback_alter(&$delivery_callback) {
  if ($router_item = menu_get_item()) {
    // Check for a node using the normal node_access callback
    if (isset($router_item['access_callback']) && $router_item['access_callback'] == 'node_access') {
      // Check for what would normally be an access denied
      if (empty($router_item['access'])) {
        // Check the status is unpublished
        if ($node = node_load($router_item['original_map']['1'])) {
          if (empty($node->status)) {
            $delivery_callback = 'mymodule_deliver_page_not_found';
          }
        }
      }
    }
  }
}
 
/**
 * Callback for mymodule_page_delivery_callback_alter()
 * to show the page not found content.
 */
function mymodule_deliver_page_not_found() {
  drupal_deliver_html_page(MENU_NOT_FOUND);
}