Serializer Error Renderer: JSON/XML Errors

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

This method is called by ErrorController and its job is to return a FlattenException that contains the status code, headers and body that should be set on the final error Response. The FlattenException::createFromThrowable intelligently sets the status code and headers. But we still need to, somehow, figure out what content to send back, like a JSON error, or an HTML page that says: "Please send help!".

Determining the Preferred Format

To do that, SerializerErrorRenderer first tries to figure out what format the user wants - like HTML or JSON. The $this->format property is actually a callback that points down here to this getPreferredFormat() method. This is a fancy way of getting the request and calling $request->getPreferredFormat(). And... hey! We know that method! I'll hit Shift + Shift and open Response.php from HttpFoundation. Search for prepare(). This method is called by a listener to the kernel.response event. It normalizes a few things... including setting the Content-Type header if it hasn't already been set. To help with that, it calls $request->getPreferredFormat() to try to figure out if the user wants HTML, JSON or something else. One of the ways it figures this out is by looking at the Accept header on the request.

Back in SerializerErrorRenderer, we're once again using $request->getPreferredFormat(), which will return a simple string like html or json.

Serializing the Error to JSON, XML, etc

Up in render(), this is pretty cool: it says:

Hey serializer! Can you try to serialize the FlattenException object into this format?

If the format is html, this will fail with a NotEncodableValueException: the serializer doesn't handle HTML. We'll talk about that case in a minute. But if the format is json, xml or some other format that the serializer does support, this will convert the exception to that format!

We can see this. If we refresh the page... we see the big HTML exception - and we'll see the code that makes this soon. Copy the URL, find your terminal and use curl to fetch that URL. But also pass a -H flag to add a header: "Accept: application/json":

curl https://localhost:8000/news/foo -H "Accept: application/json"

This will change the "preferred format" on the request to json. And... check it out! It's a 404 status code but in a JSON format! We can even use text/xml to see this in XML.

curl https://localhost:8000/news/foo -H "Accept: text/xml"

How is a FlattenException Serialized?

How does this work? One of the normalizers in the serializer is called ProblemNormalizer. I'll hit Shift + Shift to open it: ProblemNormalizer.php.

If you don't know much about the serializer component, the important thing to know is that a normalizer is responsible for taking an object and converting it into an array of data. Thanks to the supportsNormalization() method, this class is used when you try to normalize a FlattenException object.

This normalizer creates a response format that follows an HTTP specification: it helps us return an official, standardized error response. It's pretty simple: it sets keys for type, title, status and detail. In $debug mode, it also adds class and trace. Also, the detail key in debug mode will be the exception message... but in production, it will be the "status text", which is a generic "Not Found" message... or something similar, based on the status code. That's done so that your exception messages don't "leak" to the public.

The normalize() method is passed the $exception, which is the FlattenException. But if you look back at SerializerErrorRenderer, it also passes the original exception as an exception key on the $context - that's the 3rd argument to normalize().

So this gives us a really nice error response body, without any work. If you wanted to change this data, you could do that by adding your own custom normalizer. We actually talk about this in our API Platform Security Tutorial. You could decorate the ProblemNormalizer... and maybe just add or tweak some data or you could create an entirely new normalizer. Heck, you could use the $context in supports - you need to implement ContextAwareNormalizerInterface to make that work - and make that new normalizer responsible for only normalizing FlattenException classes for a specific, original exception. If you want to try that and have problems, let us know.

Ok, close that class up. Next, let's find out what happens if the format is not something that the serializer can handle. Like, HTML.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0",
        "ext-iconv": "*",
        "antishov/doctrine-extensions-bundle": "^1.4", // v1.4.2
        "aws/aws-sdk-php": "^3.87", // 3.133.20
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^2.0", // 2.0.7
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.1.2
        "doctrine/orm": "^2.5.11", // v2.7.1
        "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.8.1
        "knplabs/knp-paginator-bundle": "^5.0", // v5.1.1
        "knplabs/knp-snappy-bundle": "^1.6", // v1.7.0
        "knplabs/knp-time-bundle": "^1.8", // v1.11.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.3.0
        "nexylan/slack-bundle": "^2.1", // v2.2.2
        "oneup/flysystem-bundle": "^3.0", // 3.4.0
        "php-http/guzzle6-adapter": "^2.0", // v2.0.1
        "sensio/framework-extra-bundle": "^5.1", // v5.5.3
        "symfony/asset": "5.0.*", // v5.0.4
        "symfony/console": "5.0.*", // v5.0.4
        "symfony/dotenv": "5.0.*", // v5.0.4
        "symfony/flex": "^1.9", // v1.9.10
        "symfony/form": "5.0.*", // v5.0.4
        "symfony/framework-bundle": "5.0.*", // v5.0.4
        "symfony/mailer": "5.0.*", // v5.0.4
        "symfony/messenger": "5.0.*", // v5.0.4
        "symfony/monolog-bundle": "^3.5", // v3.5.0
        "symfony/security-bundle": "5.0.*", // v5.0.4
        "symfony/sendgrid-mailer": "5.0.*", // v5.0.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "5.0.*", // v5.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "5.0.*", // v5.0.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.7.3
        "symfony/yaml": "5.0.*", // v5.0.4
        "twig/cssinliner-extra": "^2.12", // v2.12.5
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.3.0
        "fzaninotto/faker": "^1.7", // v1.9.1
        "symfony/browser-kit": "5.0.*", // v5.0.4
        "symfony/debug-bundle": "5.0.*", // v5.0.4
        "symfony/maker-bundle": "^1.0", // v1.14.3
        "symfony/phpunit-bridge": "5.0.*", // v5.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "5.0.*" // v5.0.4
    }
}