If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThe job of this ErrorController
is to turn the Exception
that was thrown into a Response
. By the way, the error_controller
is actually configurable. So if you want to control the error response on your site, you have two options so far. First, register a listener to kernel.exception
. Or second, override the error controller via the framework.error_controller
config.
But... if you did that, you would be responsible for rendering both the normal exception pages and production error pages. If you want to change how an error page looks, there are better ways. We'll see.
Inside the __invoke()
method, the ErrorController
... is lazy! It immediately offloads the work to someone else - something called the errorRenderer
. That returns some sort of exception... which apparently has getAsString()
, getStatusCode()
and getHeaders()
methods. It uses these to create & return the Response
.
Let's... find out what this errorRenderer
thing is: dd($this->errorRenderer)
.
... lines 1 - 25 | |
class ErrorController | |
{ | |
... lines 28 - 38 | |
public function __invoke(\Throwable $exception): Response | |
{ | |
dd($this->errorRenderer); | |
... lines 42 - 44 | |
} | |
... lines 46 - 62 | |
} |
Move over and refresh. Ok cool: it's something called SerializerErrorRenderer
. And actually, it only uses this class because this project has the serializer component installed. If you did not, this would be a different class - one that we'll see in a few minutes. And, by the way, this whole "error renderer" thing is part of a Symfony component called error-handler
that's new in Symfony 4.4.
Let dig in! I'll close a class, then hit Shift + Shift to open SerializerErrorRenderer.php
. Perfect!
ErrorController
calls this render()
method, which immediately calls FlattenException::createFromThrowable
. A FlattenException
is basically a visual representation of an exception. And notice: the render()
method returns a FlattenException
.
Hold Command or Ctrl to jump into this class. Yea, see: it's not actually an exception - it doesn't extend Exception
or implement Throwable
. But it does contain a lot of the same info, like the exception $message
, $code
, $previous
and the stack trace.
The FlattenException::createFromThrowable
- if we jump to that - is a way to easily create this "visual representation" based on a real exception. And this contains some pretty important stuff. For example, if $exception
is an instance of HttpExceptionInterface
, then it calls $exception->getStatusCode()
to get the status code and $exception->getHeaders()
to get the headers. Both the status code and headers are ultimately stored on this FlattenException
object and used by ErrorController
when it creates the final Response
.
So... what is this HttpExceptionInterface
thing? We've actually seen it. Go back to ArticleController
. We know that $this->createNotFoundException()
is a shortcut to instantiate a new NotFoundHttpException
. Click to open that class... and click again to open its base class HttpException
. Here it is: HttpException
implements HttpExceptionInterface
.
This is a long way of showing you that certain exception classes in Symfony - like NotFoundHttpException
- map to a specific status code. This works because they implement HttpExceptionInterface
and because FlattenException
uses this.
Why does NotFoundHttpException
specifically map to a 404. It calls parent::__construct()
with 404... that is set to a $statusCode
property... and then returned from getStatusCode()
. You can also pass custom $headers
to the exception.
And there are a bunch of other exception classes like this. I'll double-click on the Exception
directory at the top of PhpStorm. Wow! There are more than 15 in this directory alone, like BadRequestsHttpException
, which will give you a 400 status code, PreconditionFailedHttpException
, which will be a 412 and many more. Hmm, where's the IAmATeaPotHttpException
?
If you throw any of these exceptions from anywhere in your app, they will trigger an error page with the correct status code. This is a powerful thing to understand.
Back in FlattenException
, there is also another type of exception interface called RequestExceptionInterface
. It's not as important and it always maps to a 400 status code.
If the exception doesn't implement either of these interfaces, the status code will be 500.
These are the most important parts of the FlattenException
. Close it and go back to SerializerErrorRenderer
. The job of this method is to create a FlattenException
object from the exception and make sure it contains three things that the ErrorController
needs: the status code, headers and a string representation of the error, which will become the body of the response. We've got the status code & headers... but we still need to somehow generate a "string" representation of this exception. Let's see how that's done next.
Hey,
But why? You mentioned component exception which should be handled by user code. Client exception is for some HTTP request inside your code, and it is not correct to bypass the client error code to your app error code it's the different cases. Even with bad client responses, you should have control over it.
Cheers!
I have a bit of an odd situation, where I am attempting to - under certain circumstances - change the 404 error to a 200 error.
I tried that by creating an EventSubscriber to the KernelEvens::EXCEPTION but it will accept that I change to any other error code, not a 200 type response.
public static function getSubscribedEvents()
{
return [ KernelEvents::EXCEPTION => [['notfoundException', 1000],],];
}
public function notFoundException(ExceptionEvent $event)
{
if ($event->getThrowable() instanceof NotFoundHttpException) {
// $event->setResponse(new Response('Nice not found message', Response::HTTP_FORBIDDEN)); // this works
$event->setResponse(new Response('Nice not found message', Response::HTTP_OK)); // this does not work
}
Why is it not accepting a 200 range response, and how could one accomplish that? Thanks
Hey Bernard A.!
Hmm. That's really interesting. This comes from a feature inside of Symfony that I wasn't aware of until now. Basically, when an exception is thrown, Symfony forces a 500 error... unless that status code is already a redirect, 400 or 500 level. But, this is configurable :). In your listener, call $event->allowCustomResponseCode()
. That should allow it.
To see the logic, here's that method in the event - https://github.com/symfony/symfony/blob/1fa454375eaf975d613bf6804031bf0955c6316f/src/Symfony/Component/HttpKernel/Event/ExceptionEvent.php#L60-L63 - and here is where it's used inside of HttpKernel - https://github.com/symfony/symfony/blob/1fa454375eaf975d613bf6804031bf0955c6316f/src/Symfony/Component/HttpKernel/HttpKernel.php#L236-L246
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-iconv": "*",
"antishov/doctrine-extensions-bundle": "^1.4", // v1.4.3
"aws/aws-sdk-php": "^3.87", // 3.133.20
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2.0", // 2.2.3
"doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.2.2
"doctrine/orm": "^2.5.11", // 2.8.2
"easycorp/easy-log-handler": "^1.0", // v1.0.9
"http-interop/http-factory-guzzle": "^1.0", // 1.0.0
"knplabs/knp-markdown-bundle": "^1.7", // 1.9.0
"knplabs/knp-paginator-bundle": "^5.0", // v5.4.2
"knplabs/knp-snappy-bundle": "^1.6", // v1.7.1
"knplabs/knp-time-bundle": "^1.8", // v1.16.0
"league/flysystem-aws-s3-v3": "^1.0", // 1.0.24
"league/flysystem-cached-adapter": "^1.0", // 1.0.9
"league/html-to-markdown": "^4.8", // 4.9.1
"liip/imagine-bundle": "^2.1", // 2.5.0
"oneup/flysystem-bundle": "^3.0", // 3.7.0
"php-http/guzzle6-adapter": "^2.0", // v2.0.2
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"sensio/framework-extra-bundle": "^5.1", // v5.6.1
"symfony/asset": "5.0.*", // v5.0.11
"symfony/console": "5.0.*", // v5.0.11
"symfony/dotenv": "5.0.*", // v5.0.11
"symfony/flex": "^1.9", // v1.17.5
"symfony/form": "5.0.*", // v5.0.11
"symfony/framework-bundle": "5.0.*", // v5.0.11
"symfony/mailer": "5.0.*", // v5.0.11
"symfony/messenger": "5.0.*", // v5.0.11
"symfony/monolog-bundle": "^3.5", // v3.6.0
"symfony/property-access": "5.0.*|| 5.1.*", // v5.1.11
"symfony/property-info": "5.0.*|| 5.1.*", // v5.1.10
"symfony/routing": "5.1.*", // v5.1.11
"symfony/security-bundle": "5.0.*", // v5.0.11
"symfony/sendgrid-mailer": "5.0.*", // v5.0.11
"symfony/serializer": "5.0.*|| 5.1.*", // v5.1.10
"symfony/twig-bundle": "5.0.*", // v5.0.11
"symfony/validator": "5.0.*", // v5.0.11
"symfony/webpack-encore-bundle": "^1.4", // v1.11.1
"symfony/yaml": "5.0.*", // v5.0.11
"twig/cssinliner-extra": "^2.12", // v2.14.3
"twig/extensions": "^1.5", // v1.5.4
"twig/extra-bundle": "^2.12|^3.0", // v3.3.0
"twig/inky-extra": "^2.12", // v2.14.3
"twig/twig": "^2.12|^3.0" // v2.14.4
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.4.0
"fakerphp/faker": "^1.13", // v1.13.0
"symfony/browser-kit": "5.0.*", // v5.0.11
"symfony/debug-bundle": "5.0.*", // v5.0.11
"symfony/maker-bundle": "^1.0", // v1.29.1
"symfony/phpunit-bridge": "5.0.*", // v5.0.11
"symfony/stopwatch": "^5.1", // v5.1.11
"symfony/var-dumper": "5.0.*", // v5.0.11
"symfony/web-profiler-bundle": "^5.0" // v5.0.11
}
}
Hi @weaverryan, do you know the reason behind why they don't support Symfony\Component\HttpClient\Exception\ClientException inside the FlattenException https://github.com/symfony/error-handler/blob/6.1/Exception/FlattenException.php#L54 that way any client exception would be interpreted with the right http status code instead of having 500.