Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

FlattenException & Error Status Codes

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The 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.

The SerializerErrorRenderer

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!

The All-Important FlattenException

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.

Why do Some Exceptions Cause Different Status Codes?

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.

Leave a comment!

4
Login or Register to join the conversation

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.

Reply

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!

Reply
Bernard A. Avatar
Bernard A. Avatar Bernard A. | posted 4 months ago

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

Reply

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/... - and here is where it's used inside of HttpKernel - https://github.com/symfony/...

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works well for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.0 || ^8.0.0",
        "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
    }
}