3 namespace Drupal\Core\EventSubscriber;
5 use Drupal\Component\Utility\SafeMarkup;
6 use Drupal\Core\Config\ConfigFactoryInterface;
7 use Drupal\Core\StringTranslation\StringTranslationTrait;
8 use Drupal\Core\Utility\Error;
9 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
10 use Symfony\Component\HttpFoundation\JsonResponse;
11 use Symfony\Component\HttpFoundation\Request;
12 use Symfony\Component\HttpFoundation\Response;
13 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
14 use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
15 use Symfony\Component\HttpKernel\KernelEvents;
18 * Last-chance handler for exceptions.
20 * This handler will catch any exceptions not caught elsewhere and report
21 * them as an error page.
23 class DefaultExceptionSubscriber implements EventSubscriberInterface {
24 use StringTranslationTrait;
29 * One of the error level constants defined in bootstrap.inc.
31 protected $errorLevel;
36 * @var \Drupal\Core\Config\ConfigFactoryInterface
38 protected $configFactory;
41 * Constructs a new DefaultExceptionSubscriber.
43 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
44 * The configuration factory.
46 public function __construct(ConfigFactoryInterface $config_factory) {
47 $this->configFactory = $config_factory;
51 * Gets the configured error level.
55 protected function getErrorLevel() {
56 if (!isset($this->errorLevel)) {
57 $this->errorLevel = $this->configFactory->get('system.logging')->get('error_level');
59 return $this->errorLevel;
63 * Handles any exception as a generic error page for HTML.
65 * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
66 * The event to process.
68 protected function onHtml(GetResponseForExceptionEvent $event) {
69 $exception = $event->getException();
70 $error = Error::decodeException($exception);
72 // Display the message if the current error reporting level allows this type
73 // of message to be displayed, and unconditionally in update.php.
75 if (error_displayable($error)) {
76 // If error type is 'User notice' then treat it as debug information
77 // instead of an error message.
79 if ($error['%type'] == 'User notice') {
80 $error['%type'] = 'Debug';
83 // Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path
84 // in the message. This does not happen for (false) security.
85 $root_length = strlen(DRUPAL_ROOT);
86 if (substr($error['%file'], 0, $root_length) == DRUPAL_ROOT) {
87 $error['%file'] = substr($error['%file'], $root_length + 1);
90 unset($error['backtrace']);
92 if ($this->getErrorLevel() != ERROR_REPORTING_DISPLAY_VERBOSE) {
93 // Without verbose logging, use a simple message.
95 // We call SafeMarkup::format directly here, rather than use t() since
96 // we are in the middle of error handling, and we don't want t() to
97 // cause further errors.
98 $message = SafeMarkup::format('%type: @message in %function (line %line of %file).', $error);
101 // With verbose logging, we will also include a backtrace.
103 $backtrace_exception = $exception;
104 while ($backtrace_exception->getPrevious()) {
105 $backtrace_exception = $backtrace_exception->getPrevious();
107 $backtrace = $backtrace_exception->getTrace();
108 // First trace is the error itself, already contained in the message.
109 // While the second trace is the error source and also contained in the
110 // message, the message doesn't contain argument values, so we output it
111 // once more in the backtrace.
112 array_shift($backtrace);
114 // Generate a backtrace containing only scalar argument values.
115 $error['@backtrace'] = Error::formatBacktrace($backtrace);
116 $message = SafeMarkup::format('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $error);
120 $content = $this->t('The website encountered an unexpected error. Please try again later.');
121 $content .= $message ? '</br></br>' . $message : '';
122 $response = new Response($content, 500);
124 if ($exception instanceof HttpExceptionInterface) {
125 $response->setStatusCode($exception->getStatusCode());
126 $response->headers->add($exception->getHeaders());
129 $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR, '500 Service unavailable (with message)');
132 $event->setResponse($response);
136 * Handles any exception as a generic error page for JSON.
138 * @todo This should probably check the error reporting level.
140 * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
141 * The event to process.
143 protected function onJson(GetResponseForExceptionEvent $event) {
144 $exception = $event->getException();
145 $error = Error::decodeException($exception);
147 // Display the message if the current error reporting level allows this type
148 // of message to be displayed,
150 if (error_displayable($error) && $message = $exception->getMessage()) {
151 $data = ['message' => sprintf('A fatal error occurred: %s', $message)];
154 $response = new JsonResponse($data, Response::HTTP_INTERNAL_SERVER_ERROR);
155 if ($exception instanceof HttpExceptionInterface) {
156 $response->setStatusCode($exception->getStatusCode());
157 $response->headers->add($exception->getHeaders());
160 $event->setResponse($response);
164 * Handles an HttpExceptionInterface exception for unknown formats.
166 * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
167 * The event to process.
169 protected function onFormatUnknown(GetResponseForExceptionEvent $event) {
170 /** @var \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface|\Exception $exception */
171 $exception = $event->getException();
173 $response = new Response($exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders());
174 $event->setResponse($response);
178 * Handles errors for this subscriber.
180 * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
181 * The event to process.
183 public function onException(GetResponseForExceptionEvent $event) {
184 $format = $this->getFormat($event->getRequest());
185 $exception = $event->getException();
187 $method = 'on' . $format;
188 if (!method_exists($this, $method)) {
189 if ($exception instanceof HttpExceptionInterface) {
190 $this->onFormatUnknown($event);
191 $response = $event->getResponse();
192 $response->headers->set('Content-Type', 'text/plain');
195 $this->onHtml($event);
199 $this->$method($event);
204 * Gets the error-relevant format from the request.
206 * @param \Symfony\Component\HttpFoundation\Request $request
207 * The request object.
210 * The format as which to treat the exception.
212 protected function getFormat(Request $request) {
213 $format = $request->query->get(MainContentViewSubscriber::WRAPPER_FORMAT, $request->getRequestFormat());
215 // These are all JSON errors for our purposes. Any special handling for
216 // them can/should happen in earlier listeners if desired.
217 if (in_array($format, ['drupal_modal', 'drupal_dialog', 'drupal_ajax'])) {
221 // Make an educated guess that any Accept header type that includes "json"
222 // can probably handle a generic JSON response for errors. As above, for
223 // any format this doesn't catch or that wants custom handling should
224 // register its own exception listener.
225 foreach ($request->getAcceptableContentTypes() as $mime) {
226 if (strpos($mime, 'html') === FALSE && strpos($mime, 'json') !== FALSE) {
235 * Registers the methods in this class that should be listeners.
238 * An array of event listener definitions.
240 public static function getSubscribedEvents() {
241 $events[KernelEvents::EXCEPTION][] = ['onException', -256];