k0pernikus
k0pernikus

Reputation: 66500

How to format all HttpExceptions as json in symfony5?

Within a symfony5 controller, I can return json responses via:

 return $this->json(['key' => 'content');

Yet when I throw an HttpException, I see the default html error page in both dev and production.

I want to create a restful api, so I want to convert all HttpExceptions into json.

I want to configure all my controllers to format their response as json. At most, I want to add one Exception handler that would transform the excpetions into proper messages. (In prod it should have less information, in dev it may contain the exception stacktrace.)

How can I achieve this? I thought I could use the format option of the @Route annotation but it doesn't work.

This is my example controller:

<?php declare(strict_types=1);

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;

class StatusController extends AbstractController
{
    /**
     * @Route("/status", name="status", format="json")
     * @Template
     * @return JsonResponse
     */
    public function status()
    {
        if (true) {
            // this will render as html, how to serialize it as json?
            throw new NotFoundHttpException("This is an example");
        }


        $ok = new \stdClass();
        $ok->status = "OK";

        return $this->json($ok);
    }
}

While looking for this I came across this PR which seems to achieve what I am trying to do, yet I am unsure what I am missing.

On the symfony blog I found following answer by Yonel Ceruto saying

you will need to install/enable the serializer component,

yet I have no idea what this entails.


In dev and prod I got these html views instead of a json response:

prod

prod view of html for rendered exceptions

dev

dev view of html view for rendered exceptions

Upvotes: 7

Views: 14701

Answers (4)

andrea porcella
andrea porcella

Reputation: 21

In an API development context the simplest and most effective thing is to Override the Default ErrorController. This allows you to format the error output according to the JSON Api standard used in your application (Jsend on the example)

See the documentation here

First create your ApiErrorController :

php bin/console make:controller --no-template ApiErrorController

Second set the framework configuration to use it:

framework: 
...

error_controller: App\Controller\ApiErrorController::show

Third edit your ApiErrorController :

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Psr\Log\LoggerInterface;
use Throwable;

class ApiErrorController extends AbstractController
{
    #[Route('/api/error', name: 'app_api_error')]
    public function show(Throwable $exception, LoggerInterface $logger): JsonResponse
    {
       
        //Transform Warning status code from 0 to 199 
        $statusCode = (!$exception->getCode() || $exception->getCode()==0)? 199 : $exception->getCode() ;
        
        //Log the error
        if($statusCode<=199){ 
            $logger->warning($exception->getMessage());
        } else {
            $logger->error($exception->getMessage());
        }

        //Prepare basic data output coforming on JSend STD.
        $data = [
            'status'=>($statusCode>=400)? 'error' : 'fail',
            'message' => $exception->getMessage(),
        ];

        //If Dev add trace
        if ($this->getParameter('kernel.environment') === 'dev') {
            $data['trace'] = $exception->getTrace();
        }

        //return Json
        return $this->json($data, $statusCode);
    }
}

Upvotes: 2

Pascal Tovohery
Pascal Tovohery

Reputation: 986

Create a event listener ExceptionListener and register in services.yml

services:
    ...
    App\EventListener\ExceptionListener:
        tags:
            - { name: kernel.event_listener, event: kernel.exception }

ExceptionLister.php

// src/EventListener/ExceptionListener.php
namespace App\EventListener;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

class ExceptionListener
{
    public function onKernelException(ExceptionEvent $event)
    {
        // You get the exception object from the received event
        $exception = $event->getThrowable();
        // Get incoming request
        $request   = $event->getRequest();

        // Check if it is a rest api request
        if ('application/json' === $request->headers->get('Content-Type'))
        {

            // Customize your response object to display the exception details
            $response = new JsonResponse([
                'message'       => $exception->getMessage(),
                'code'          => $exception->getCode(),
                'traces'        => $exception->getTrace()
            ]);

            // HttpExceptionInterface is a special type of exception that
            // holds status code and header details
            if ($exception instanceof HttpExceptionInterface) {
                $response->setStatusCode($exception->getStatusCode());
                $response->headers->replace($exception->getHeaders());
            } else {
                $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
            }

            // sends the modified response object to the event
            $event->setResponse($response);
        }
    }
}

Upvotes: 4

Toknsit Toknsit
Toknsit Toknsit

Reputation: 144

The package symfony/serializer-pack not work in my environment.

Finally I create A ErrorController to response json .

namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Throwable;

class JsonErrorController
{
    public function show(Throwable $exception, LoggerInterface $logger)
    {
        return new JsonResponse($exception->getMessage(), $exception->getCode());
    }
}

Upvotes: 0

k0pernikus
k0pernikus

Reputation: 66500

Turns out all I was missing was installing the serializer-pack as pointed out in the symfony docs:

composer require symfony/serializer-pack

Afterwards, my exceptions render as json fine.

Upvotes: 10

Related Questions