DefaultExceptionHtmlSubscriber.php

Same filename in other branches
  1. 9 core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
  2. 8.9.x core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
  3. 10 core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php

Namespace

Drupal\Core\EventSubscriber

File

core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php

View source
<?php

namespace Drupal\Core\EventSubscriber;

use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\Core\Utility\Error;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;

/**
 * Exception subscriber for handling core default HTML error pages.
 */
class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
    
    /**
     * The HTTP kernel.
     *
     * @var \Symfony\Component\HttpKernel\HttpKernelInterface
     */
    protected $httpKernel;
    
    /**
     * The logger instance.
     *
     * @var \Psr\Log\LoggerInterface
     */
    protected $logger;
    
    /**
     * The redirect destination service.
     *
     * @var \Drupal\Core\Routing\RedirectDestinationInterface
     */
    protected $redirectDestination;
    
    /**
     * A router implementation which does not check access.
     *
     * @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
     */
    protected $accessUnawareRouter;
    
    /**
     * Constructs a new DefaultExceptionHtmlSubscriber.
     *
     * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
     *   The HTTP kernel.
     * @param \Psr\Log\LoggerInterface $logger
     *   The logger service.
     * @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
     *   The redirect destination service.
     * @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $access_unaware_router
     *   A router implementation which does not check access.
     */
    public function __construct(HttpKernelInterface $http_kernel, LoggerInterface $logger, RedirectDestinationInterface $redirect_destination, UrlMatcherInterface $access_unaware_router) {
        $this->httpKernel = $http_kernel;
        $this->logger = $logger;
        $this->redirectDestination = $redirect_destination;
        $this->accessUnawareRouter = $access_unaware_router;
    }
    
    /**
     * {@inheritdoc}
     */
    protected static function getPriority() {
        // A very low priority so that custom handlers are almost certain to fire
        // before it, even if someone forgets to set a priority.
        return -128;
    }
    
    /**
     * {@inheritdoc}
     */
    protected function getHandledFormats() {
        return [
            'html',
        ];
    }
    
    /**
     * Handles a 4xx error for HTML.
     *
     * @param \Symfony\Component\HttpKernel\Event\ExceptionEvent $event
     *   The event to process.
     */
    public function on4xx(ExceptionEvent $event) {
        if (($exception = $event->getThrowable()) && $exception instanceof HttpExceptionInterface) {
            $this->makeSubrequest($event, '/system/4xx', $exception->getStatusCode());
        }
    }
    
    /**
     * Handles a 400 error for HTML.
     *
     * @param \Symfony\Component\HttpKernel\Event\ExceptionEvent $event
     *   The event to process.
     */
    public function on400(ExceptionEvent $event) : void {
        throw $event->getThrowable();
    }
    
    /**
     * Handles a 401 error for HTML.
     *
     * @param \Symfony\Component\HttpKernel\Event\ExceptionEvent $event
     *   The event to process.
     */
    public function on401(ExceptionEvent $event) {
        $this->makeSubrequest($event, '/system/401', Response::HTTP_UNAUTHORIZED);
    }
    
    /**
     * Handles a 403 error for HTML.
     *
     * @param \Symfony\Component\HttpKernel\Event\ExceptionEvent $event
     *   The event to process.
     */
    public function on403(ExceptionEvent $event) {
        $this->makeSubrequest($event, '/system/403', Response::HTTP_FORBIDDEN);
    }
    
    /**
     * Handles a 404 error for HTML.
     *
     * @param \Symfony\Component\HttpKernel\Event\ExceptionEvent $event
     *   The event to process.
     */
    public function on404(ExceptionEvent $event) {
        $this->makeSubrequest($event, '/system/404', Response::HTTP_NOT_FOUND);
    }
    
    /**
     * Makes a subrequest to retrieve the default error page.
     *
     * @param \Symfony\Component\HttpKernel\Event\ExceptionEvent $event
     *   The event to process.
     * @param string $url
     *   The path/url to which to make a subrequest for this error message.
     * @param int $status_code
     *   The status code for the error being handled.
     */
    protected function makeSubrequest(ExceptionEvent $event, $url, $status_code) {
        $request = $event->getRequest();
        $exception = $event->getThrowable();
        try {
            // Reuse the exact same request (so keep the same URL, keep the access
            // result, the exception, et cetera) but override the routing information.
            // This means that aside from routing, this is identical to the master
            // request. This allows us to generate a response that is executed on
            // behalf of the master request, i.e. for the original URL. This is what
            // allows us to e.g. generate a 404 response for the original URL; if we
            // would execute a subrequest with the 404 route's URL, then it'd be
            // generated for *that* URL, not the *original* URL.
            $sub_request = clone $request;
            // The routing to the 404 page should be done as GET request because it is
            // restricted to GET and POST requests only. Otherwise a DELETE request
            // would for example trigger a method not allowed exception.
            $request_context = clone $this->accessUnawareRouter
                ->getContext();
            $request_context->setMethod('GET');
            $this->accessUnawareRouter
                ->setContext($request_context);
            $sub_request->attributes
                ->add($this->accessUnawareRouter
                ->match($url));
            // Add to query (GET) or request (POST) parameters:
            // - 'destination' (to ensure e.g. the login form in a 403 response
            //   redirects to the original URL)
            // - '_exception_statuscode'
            $parameters = $sub_request->isMethod('GET') ? $sub_request->query : $sub_request->request;
            $parameters->add($this->redirectDestination
                ->getAsArray() + [
                '_exception_statuscode' => $status_code,
            ]);
            $response = $this->httpKernel
                ->handle($sub_request, HttpKernelInterface::SUB_REQUEST);
            // Only 2xx responses should have their status code overridden; any
            // other status code should be passed on: redirects (3xx), error (5xx)…
            // @see https://www.drupal.org/node/2603788#comment-10504916
            if ($response->isSuccessful()) {
                $response->setStatusCode($status_code);
            }
            // Persist the exception's cacheability metadata, if any. If the exception
            // itself isn't cacheable, then this will make the response uncacheable:
            // max-age=0 will be set.
            if ($response instanceof CacheableResponseInterface) {
                $response->addCacheableDependency($exception);
            }
            // Persist any special HTTP headers that were set on the exception.
            if ($exception instanceof HttpExceptionInterface) {
                $response->headers
                    ->add($exception->getHeaders());
            }
            $event->setResponse($response);
        } catch (\Exception $e) {
            // If an error happened in the subrequest we can't do much else. Instead,
            // just log it. The DefaultExceptionSubscriber will catch the original
            // exception and handle it normally.
            $error = Error::decodeException($e);
            $this->logger
                ->log($error['severity_level'], Error::DEFAULT_ERROR_MESSAGE, $error);
        }
    }

}

Classes

Title Deprecated Summary
DefaultExceptionHtmlSubscriber Exception subscriber for handling core default HTML error pages.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.