FOSRestBundle icon indicating copy to clipboard operation
FOSRestBundle copied to clipboard

overwrite JMS's ExceptionHandler (FOS\RestBundle\Serializer\Normalizer\ExceptionHandler)

Open ebeem opened this issue 8 years ago • 13 comments

I am trying to create custom handlers (I am using JMS-Serializer) for different exceptions to make them all output a similar json structure response. I have successfully overwritten HttpException exceptions and it works like a charm. I need some other exceptions that I tried to handle, but the handlers that I created are just ignored. I created handlers for HttpException, OAuth2AuthenticateException, OAuth2ServerException, \Exception

all don't work except for the HttpException handler.

services.yaml

parameters:
    #parameter_name: value

services:
    jms_serializer.http_exception_handler:
        class:     AppBundle\Handler\HttpExceptionHandler
        tags:
        - { name: jms_serializer.subscribing_handler, priority: 999999999 }
        arguments: ["@fos_rest.exception.messages_map", false]

    jms_serializer.oauth_server_exception_handler:
        class:     AppBundle\Handler\OAuth2ServerExceptionHandler
        tags:
        - { name: jms_serializer.subscribing_handler, priority: 999999999 }
        arguments: ["@fos_rest.exception.messages_map", false]

    jms_serializer.oauth_authenticate_exception_handler:
        class:     AppBundle\Handler\OAuth2AuthenticateExceptionHandler
        tags:
        - { name: jms_serializer.subscribing_handler, priority: 999999999 }
        arguments: ["@fos_rest.exception.messages_map", false]

    jms_serializer.other_exception_handler:
            class:     AppBundle\Handler\ExceptionHandler
            tags:
            - { name: jms_serializer.subscribing_handler, priority: 999999999 }
            arguments: ["@fos_rest.exception.messages_map", false]


note: I tried tags with and without priority: 999999999, both have same behaviour

content of my ExceptionHandler:

<?php

namespace AppBundle\Handler;


use FOS\RestBundle\Serializer\Normalizer\AbstractExceptionNormalizer;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\XmlSerializationVisitor;

class ExceptionHandler extends AbstractExceptionNormalizer implements SubscribingHandlerInterface
{
    /**
     * @return array
     */
    public static function getSubscribingMethods()
    {
        return [
            [
                'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
                'format' => 'json',
                'type' => \Exception::class,
                'method' => 'serializeToJson',
            ],
            [
                'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
                'format' => 'xml',
                'type' => \Exception::class,
                'method' => 'serializeToXml',
            ],
        ];
    }

    /**
     * @param JsonSerializationVisitor $visitor
     * @param Exception                $exception
     * @param array                    $type
     * @param Context                  $context
     *
     * @return array
     */
    public function serializeToJson(
        JsonSerializationVisitor $visitor,
        \Exception $exception,
        array $type,
        Context $context
    ) {
        $data = $this->convertToArray($exception, $context);

        return $visitor->visitArray($data, $type, $context);
    }

    /**
     * @param XmlSerializationVisitor $visitor
     * @param Exception               $exception
     * @param array                   $type
     * @param Context                 $context
     */
    public function serializeToXml(
        XmlSerializationVisitor $visitor,
        \Exception $exception,
        array $type,
        Context $context
    ) {
        $data = $this->convertToArray($exception, $context);

        if (null === $visitor->document) {
            $visitor->document = $visitor->createDocument(null, null, true);
        }

        foreach ($data as $key => $value) {
            $entryNode = $visitor->document->createElement($key);
            $visitor->getCurrentNode()->appendChild($entryNode);
            $visitor->setCurrentNode($entryNode);

            $node = $context->getNavigator()->accept($value, null, $context);
            if (null !== $node) {
                $visitor->getCurrentNode()->appendChild($node);
            }

            $visitor->revertCurrentNode();
        }
    }

    /**
     * @param \Exception $exception
     *
     * @return array
     */
    protected function convertToArray(\Exception $exception, Context $context)
    {
        $data = [];

        $templateData = $context->attributes->get('template_data');
        if ($templateData->isDefined()) {
            $data['code1'] = $statusCode = $templateData->get()['status_code'];
        }

        $data['message1'] = $this->getExceptionMessage($exception, isset($statusCode) ? $statusCode : null);

        return $data;
    }
}

it's exactly similar to FOS\RestBundle\Serializer\Normalizer\ExceptionHandler.php the only difference is the data fields are code1, message1 instead of code, message

I tried also to uncomment all the other services:

services:
    jms_serializer.other_exception_handler:
            class:     AppBundle\Handler\ExceptionHandler
            tags:
            - { name: jms_serializer.subscribing_handler, priority: 999999999 }
            arguments: ["@fos_rest.exception.messages_map", false]

but the handler is never called. (of course I tried restarting the server and deleting the cache)

The behavior I excpected is that since I am making new handlers, they will be called in their order.

AppBundle\Handler\HttpExceptionHandler
AppBundle\Handler\OAuth2ServerExceptionHandler
AppBundle\Handler\OAuth2AuthenticateExceptionHandler
AppBundle\Handler\ExceptionHandler
FOS\RestBundle\Serializer\Normalizer\ExceptionHandler

the current behavior looks a bit weird, OAuth2ServerExceptionHandler & OAuth2AuthenticateExceptionHandler are never handeled

the order of other exceptions handling is

AppBundle\Handler\HttpExceptionHandler
FOS\RestBundle\Serializer\Normalizer\ExceptionHandler
AppBundle\Handler\ExceptionHandler

I could not find many topics regarding this topic, I hope someone can explain what's wrong with my code.

my AppKernel order

            new JMS\SerializerBundle\JMSSerializerBundle(),
            new AppBundle\AppBundle(),
            // ...
            new FOS\RestBundle\FOSRestBundle(),
            // ..

my config.yml

jms_serializer:
    handlers:
        datetime:
            default_format: "c" # ISO8601
            default_timezone: "UTC"
    property_naming:
            separator:  _
            lower_case: true
    metadata:
        auto_detection: true
        directories:
            FOSUserBundle:
                namespace_prefix: "FOS\\UserBundle"
                path: "@AppBundle/Resources/config/serializer/fos"

fos_rest:
    param_fetcher_listener: true
    view:
        view_response_listener: force
        mime_types:
            json: ['application/json', 'application/json;version=1.0', 'application/json;version=1.1']
        view_response_listener: 'force'
        formats:
            xml:  false
            json: true
        templating_formats:
            html: true
    format_listener:
        rules:
            - { path: "^/v[0-9]", priorities: [ json ], fallback_format: json, prefer_extension: true }
            - { path: "^/oauth", priorities: [ json ], fallback_format: json, prefer_extension: true }
    exception:
        enabled: true

    allowed_methods_listener: true
    access_denied_listener:
        json: true
    body_listener: true
    serializer:
          serialize_null: true

I am not sure if it's going to make a difference, it's recommended to load JMSSerializerBundle before FOSRestBundle I tried to play with the order a little bit, but with no luck.

symfony version: v2.8.22 FOSRest-Bundle version: v2.2.0 JMS-Serilizer-Bundle version: v2.0.0

I tried dev-master version for both FOSRest-Bundle & JMS-Serilizer-Bundle as well.

ebeem avatar Jun 07 '17 21:06 ebeem

still don't know how does it work, but when I named my service the same name as the FOSRestBundle's one it worked.

fos_rest.serializer.exception_normalizer.jms:
            class:     AppBundle\Handler\ExceptionHandler
            tags:
            - { name: jms_serializer.subscribing_handler }
            arguments: ["@fos_rest.exception.messages_map", false]

ebeem avatar Jun 08 '17 09:06 ebeem

@ebeem if you reused the same name, it replaced the service entirely

stof avatar Jun 08 '17 12:06 stof

@stof so what can I do to handle/normalize an exception using JMS? and why only HttpException is handled?

ebeem avatar Jun 08 '17 13:06 ebeem

@ebeem Hi, how/where did you define your service with the same name as "fos_rest.serializer.exception_normalizer.jms"? because this service depends on service "@fos_rest.exception.messages_map" which is not public in this bundle

so far I can not find any way to override the jms exception handler without writing all the dependencies @stof is it possible to add some example in the documentation about how to customize exception handler/normalizer for people who use jms serilaizer?

Related to https://github.com/FriendsOfSymfony/FOSRestBundle/issues/1379

phoenixgao avatar Jun 28 '17 05:06 phoenixgao

@phoenixgao services.yml

ebeem avatar Jun 28 '17 07:06 ebeem

@phoenixgao private services can perfectly be used as a dependency (the container itself does not isolate services by bundle, there is no such concept). The only thing being forbidden for private services is runtime retrieval with $container->get('my_id') (this restriction is what allows us to optimize the container, as all other accesses to the service are known at compile time)

stof avatar Jun 28 '17 08:06 stof

Perfect! Thanks! @ebeem @stof

phoenixgao avatar Jun 28 '17 09:06 phoenixgao

Hello, it seems that the bug still exists and I need to name my service fos_rest.serializer.exception_normalizer.jms in order to override the custom exception handler.

Can you confirm that?

Thanks.

lukepass avatar Jul 28 '17 08:07 lukepass

@lukepass hey, sorry, but I switched to another framework (Java spring). I did not investigate the issue, but I think it's a bug.

ebeem avatar Jul 28 '17 16:07 ebeem

I wonder if anyone else had that issue and managed to find a solution without overriding fos_rest.serializer.exception_normalizer.jms

sela avatar Oct 12 '18 14:10 sela

I am still doing this in my recent projects!

lukepass avatar Oct 12 '18 15:10 lukepass

You should declare priority of method in array returned from getSubscribingMethods:

<?php

namespace App\Serializer\Handler;

use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use Exception;

class ExceptionSubscribingHandler implements SubscribingHandlerInterface
{
    public static function getSubscribingMethods(): array
    {
        return [
            [
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'format' => 'json',
                'type' => Exception::class,
                'method' => 'serializeToJson',
                'priority' => -1, // defaults to 0, lowest priority gets used
            ],
        ];
    }

    public function serializeToJson(JsonSerializationVisitor $visitor, Exception $exception, array $type, Context $context)
    {
        return 'foo';
    }
}

For anyone curious why - take a look at class CustomHandlersPass in JMSSerializerBundle.

hxv avatar Aug 08 '19 04:08 hxv

Pretty old thread, but I thought to share another possible explanation/solution.

The SerializerErrorRenderer converts Throwables to FlattenException, so you cannot catch your own Exception class via 'type' => CustomException::class. You can either catch Exception directly or work with FlattenException. Combined with the priority flag mentioned by @hxv the following works now for me:

    public function __construct(FlattenExceptionHandler $exceptionHandler) {
        $this->exceptionHandler = $exceptionHandler;
    }

    public static function getSubscribingMethods() {
        return [[
            'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
            'type' => FlattenException::class,
            'format' => 'json',
            'method' => 'serializeExceptionToJson',
            'priority' => -1
        ]];
    }

    public function serializeExceptionToJson(JsonSerializationVisitor $visitor, FlattenException $exception, array $type, Context $context) {
        if ($exception->getClass() !== CustomException::class) {
            return $this->exceptionHandler->serializeToJson($visitor, $exception, $type, $context);
        }
       // ... your conversion code
    }

kevinpapst avatar Jan 20 '21 16:01 kevinpapst