Symfony2, FOSRestBundle - catch exceptions - php

I have Symfony application and I use FOSRestBundle with AngularJS. My Symfony application haven't any views.
I want to show in AngularJS messages about information received from server, with ngToast module.
If I create something or update it it's easy to show. But If server throw some exception? For example Angular client trying get item with wrong ID or this user have no acces to do this action? In this case server will throw exception, but I want to show appropriate message.
Can symfony catch this exception and for example translate it to Response object?
For example - if I have no access exception symfony should catch it and make something like this:
return new Response(400, "You don't have permission to acces this route");
and Angular will get:
{
"code": 400,
"message": "You don't have permission to acces this route"
}
Is it possible? How should I do it? Maybe I should do it other way.

I would suggest to go for more FOSRestBundle approach, which is to configure the Exceptions and if should or not show the messages:
fos_rest:
exception:
enabled: true
codes:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': HTTP_FORBIDDEN
messages:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': true
Lets say you have a custom AccessDeniedException for a certain action, you can create the exception and then put it in the configuration.
<?php
class YouCantSummonUndeadsException extends \LogicException
{
}
Wherever you throw it:
<?php
throw new YouCantSummonUndeadsException('Denied!');
You can configure it:
codes:
'My\Custom\YouCantSummonUndeadsException': HTTP_FORBIDDEN
messages:
'My\Custom\YouCantSummonUndeadsException': true
And get a result like:
{
"code": 403,
"message": "Denied!"
}
I hope this makes it clearer!

Yes of course this is possible. I suggest you implement a simple Exception listener. And make all your exception classes extend a BaseException or implement a BaseException, so you will know which exceptions are from "your" code.
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
// You get the exception object from the received event
$exception = $event->getException();
// Do your stuff to create the response, for example response status code can be exception code, and exception message can be in the body or serialized in json/xml.
$event->setResponse($response);
return;
}
}
Register it in the container:
<service id="your_app.listener.exception" class="App\ExceptionListener">
<tag name="kernel.event_listener" event="kernel.exception" method="onKernelException" />
</service>

Related

How to avoid logging expected exceptions that are correctly converted to status codes by Api-Platform?

In some of routes for our Api-Platform project, exceptions are thrown for some common error conditions.
E.g. while calling POST /orders, NewOrderHandler can throw either of this two if appropriate:
NotEnoughStock
NotEnoughCredit
All these exceptions belong to a DomainException hierarchy.
These exceptions are correctly converted to status code 400 on the response by using the exception_to_status configuration, and the response includes the appropriate error message. So far so good.
exception_to_status:
App\Order\NotEnoughStock: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST
App\Order\NotEnoughCredit: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST
The only problem is that the exception is still logged as a CRITICAL error, being treated as an "uncaught exception". This gets logged even in production.
I would have expected that by being converted to a correct status code (e.g. !== 500) these exceptions would be treated as "handled", and thus would not pollute the logs.
Throwing exceptions from the handler is convenient, because it helps deal with transactionality and automatically generates the appropriate error response message. It works for web and console.
Shouldn't these transactions be treated as handled? Is it necessary to create another exception listener to deal with this? And if creating an exception listener, how to do it so it doesn't interfere with Api-Platform error normalization?
There is a simple answer: handling an exception is not catching an exception.
Even if you convert your exception to a 400 error, your exception is still uncaught... that's why symfony logs it and this is done here.
If you don't want to log any DomainException, just override the logException() method in order to skip logging if it's an instance of DomainException.
Here is an exemple:
namespace App\EventListener;
use Symfony\Component\HttpKernel\EventListener\ErrorListener;
class ExceptionListener extends ErrorListener
{
protected function logException(\Exception $exception, string $message): void
{
if ($exception instanceof DomainException) {
return;
}
parent::logException($exception, $message);
}
}
Finally you need to tell Symfony to use this class instead of the Symfony one. Since there is no class parameter for the exception_listener service definition I recommend to use a compiler pass in order to replace the class.
namespace App;
use App\EventListener\ExceptionListener;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class OverrideServiceCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('exception_listener');
$definition->setClass(ExceptionListener::class);
}
}
See Bundle override for more details.
Alternatively, just decorate the exception_listener service with your own and no compiler pass is needed:
App\EventListener\ExceptionListener:
decorates: 'exception_listener'
I tested it on a dummy application and I got:
Apr 11 21:36:11 |CRITI| REQUES Uncaught PHP Exception App\Exception\DomainException: "This is no more logged" at D:\www\campagne\src\DataPersister\StationDataPersister.php line 53
Apr 11 23:36:12 |WARN | SERVER POST (400) /api/stations
You could implement your own log activation strategy:
This code is based on the HttpCode activation strategy
namespace App\Log
use App\Exception\DomainException;
use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* Activation strategy for logs
*/
class LogActivationStrategy extends ErrorLevelActivationStrategy
{
public function __construct()
{
parent::__construct('error');
}
public function isHandlerActivated(array $record): bool
{
$isActivated = parent::isHandlerActivated($record);
if ($isActivated && isset($record['context']['exception'])) {
$exception = $record['context']['exception'];
// This is a domain exception, I don't log it
return !$exception instanceof DomainException;
// OR if code could be different from 400
if ($exception instanceof DomainException) {
// This is a domain exception
// You log it when status code is different from 400.
return 400 !== $exception->getStatusCode();
}
}
return $isActivated;
}
}
We also need to tell Monolog to use our ActivationStrategy
monolog:
handlers:
main:
type: fingers_crossed
action_level: info
handler: nested
activation_strategy: App\Log\LogActivationStrategy
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
Now my log contains only :
Apr 11 23:41:07 |WARN | SERVER POST (400) /api/stations
Like #yivi, I'm not in love of my solution, because each time application will try to log something, you loose time in this function... And this method do not change the log, it removes it.
While in Monolog, when using the fingers_crossed log handler, will let you exclude from logging requests that respond with certain statuses, it will only do so if the exception is an instance of HttpException:
I got around this by implementing a subscriber to convert the exception into an BadRequestHttpException.
final class DomainToHttpExceptionSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): iterable
{
return [ KernelEvents::EXCEPTION => 'convertException'];
}
public function convertException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
if ($exception instanceof DomainException) {
$event->setThrowable(
new BadRequestHttpException(
$exception->getMessage(),
$exception
)
);
}
}
}
This coupled with this monolog configuration does the trick:
monolog:
handlers:
fingers:
type: fingers_crossed
action_level: warning
excluded_http_codes:
- 404
- 400
I got this from this answer on a GitHub issue. It works, but I'm not in love the solution. Hopefully some other answer will come to improve on this.

Customize Laravel FormRequest autorize method validation

Laravel allows us to authorize, or not, a FormRequest to be processed via the authorize method. If the request isn't authorized, it will throw a \Illuminate\Auth\Access\AuthorizationException exception, with a message:
This action is unauthorized.
Is there somehow to customize this message?
See that I want to customize the message itself. Customizing the error messages of attributes I know it is possible!
To change the message you can add the following to your FormRequest class.
protected function failedAuthorization()
{
throw new AuthorizationException('Your new message goes here.');
}
if you are trying to customise the message authourization exceptional message then use throw new exception in authorization controller itself in else part

Symfony: cannot always catch exception

Sorry for this vague title, I didn't know how to title my question.
I'm listening on kernel.exception via the kernel.event_listener service. I use it in my API to catch all exceptions and serialize them in JSON for a clean error handling for the API customers.
I have to adapt the serialization depending on the exception types (my HTTP exceptions, Symfony HTTP exceptions, and others).
When a user is not authenticated when accessing a section restricted by access_control in security.yml, Symfony throws a non-HTTP Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException. In my serializer, a non-HTTP exception is converted in a 500 error. Since an InsufficientAuthenticationException is rather a 401 Unauthorized error, I have to catch this exception separately and convert it in my app-specific exception type.
Example:
# Find appropriate serialization
if($ex instanceof HttpErr\HttpErrorInterface) {
# AppBundle\Exceptions\Http\*
# A displayable error thrown intentionally by our controllers
$status = $ex->getStatusCode();
$message = $ex->getMessage();
$other = $ex->getAdditionalDatas();
} elseif($ex instanceof InsufficientAuthenticationException) {
throw new HttpErr\EUnauthorized; # look a this line
}
# more elseifs...
That works. The Symfony authentication exception is catched, then converted in EUnauthorized, and then EUnauthorized is serialized into JSON. But you can see that I throw the exception without message or previous exception.
Because I want to do this:
elseif($ex instanceof InsufficientAuthenticationException) {
# the standard argument $previous is in 2nd position in my exceptions instead of being 3rd.
# the previous argument is important for me since it will keep context in error propagation.
throw new HttpErr\EUnauthorized($ex->getMessage(), $ex);
}
When I do this (so, just adding two arguments), the serialization stops working, my event listener is not called and the app crashes (in prod, this will turn into a friendly WSoD):
Why?
In the first "if" you extract data for serialization, in the second you are just rethrowing a new exception.
This new exception does not go the kernel.exception flow anymore. It is correctly just thrown: as you can see you have the full stack of exceptions shown.
Ideally, you should end your onKernelException with some kind of Response.
EDIT
I'll expand a bit my previous answer with references to the Symfony documentation and code.
The HttpKernel docs say
If an exception is thrown at any point inside HttpKernel::handle, another event - kernel.exception is thrown. Internally, the body of the handle function is wrapped in a try-catch block. When any exception is thrown, the kernel.exception event is dispatched so that your system can somehow respond to the exception.
So, your listener is called after an exception in the handle function, but, as you can see in source no try/catch is provided by the handleException function. This basically means that an Exception thrown in your listener should not be caught.
In your listener you could swap the current exception with a new one with $event->setException(...) or just try to build a Response yourself.
In any case, throwing a new Exception does not seem the proper way here. I sadly don't know why you code works with or without parameters without all the code involved.
I don't know if it helps here, but I had similar problem my event listener is not called and the app crashes. So i worked around that and overrided one method in Kernel.php file like that:
protected function initializeContainer() {
try {
$container = parent::initializeContainer();
} catch (\Throwable $throwable){
// MY CATCH CODE GOES HERE
}
return $container;
}
You can also hook up to other Kernel methods and override them.
Notice: I'm using Symfony 4.2.*

How can I pass a error 500 message to my error template?

Using Syfmony2, I have created custom error templates, in my case it is
/app/Resources/TwigBundle/views/Exception/error.html.twig
and looks somehow like this
<html>
</body>
<h1>ERROR {{ status_code }}</h1>
<p>{{ status_text }}</p>
</body>
</html>
When I now throw an error with a message in it:
throw new Exception('There is something wrong in the state of Denkmark.');
I would expect that the message is shown on the rendered error template. Instead, it only shows a standard message:
Internal Server Error
However, when I run in dev mode it shows the correct message (but on the Symfony2 standard error template). Why is the message in prod mode hidden? Who am I writing the messages for? For the dev log?
(How) can I force to show the message on my template in prod mode as well?
This behaviour is correct as Exception may contains some "internal" information and in production enviroment those infos should not be displayed.
What you can do is costumize the 404 page or use your own logic to display something when an exception occurs but you can't rely on symfony2 standard logic as it's not compatible with your
For costumize 404 page, you should ovveride default template by placing your own error page here app/Resources/TwigBundle/views/Exception/error404.html.twig
For your own logic: simply use an event listener/subscriber that will render a page upon some exceptions
You will nedd to make a custom template, EventListener and register the EventListener:
// src/Acme/DemoBundle/EventListener/AcmeExceptionListener.php
namespace Acme\DemoBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class AcmeExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
// You get the exception object from the received event
$exception = $event->getException();
$message = sprintf(
'My Error says: %s with code: %s',
$exception->getMessage(),
$exception->getCode()
);
// Customize your response object to display the exception details
$response = new Response();
$response->setContent($message);
// 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);
}
// Send the modified response object to the event
$event->setResponse($response);
}
}
and you will need to register the listenter:
# app/config/config.yml
services:
kernel.listener.your_listener_name:
class: Acme\DemoBundle\EventListener\AcmeExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception, method:
onKernelException }
source:
http://symfony.com/doc/current/cookbook/service_container/event_listener.html

Laravel Handle Exceptions

I am using the Stripe API and Laravel together. If Stripe detects an error charging the card (such as using a test credit card number that throws an invalid security code error), the API bindings are supposed to throw an exception, which they do. The problem is, I am having issues catching the exception before Laravel throws up the error 500 page (I am trying to perform a redirect with an error message instead).
The code I've written is available on Pastebin: http://pastebin.com/ZaW2xbbt
The behavior I'm expecting is for the catch to fire and the redirect to be performed, but instead, I get the stack trace with the message and "Unhandled Exception". That's confusing me because I am handling the exception.
Variables such as $customer are valid and have been defined previously. Any ideas what's going on?
For any future viewers, here's an article on error handling in laravel 4.
Laravel 4 lets you catch Exceptions by exception type. For instance, you can handle Symfony's HttpException and of its sub-classes by adding this to your code:
// Catch HttpException, NotFoundHttpException, etc etc
App::error(function(HttpException $exception, $code, $fromConsole)
{
...
});
Symfony HttpExceptions (used in Laravel) can be found here.
You can also throw this in a ServiceProvider:
<?php namespace My\Namespace;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\HttpKernel\Exception\HttpException;
class MyServiceProvider extends ServiceProvider {
public function register()
{
$this->app->error(function(HttpException $exception, $code, $fromConsole)
{
...
});
}
}
Hope that helps!
Generally, all errors logged by Laravel are logged under storage/logs folder
Anyway, the 500 error could be a syntax/parse error, in such case the Laravel framework could be not yet loaded when the error occurs and if so, the exception is not lhandled by Laravel.
In this case you should access the apache/vargrant/whatif php error log in some way (dependently on your server capabilities and configuration), in my personal cases I have configured the server to put that logs in a /storage/logs/error_log.txt file such that I can access them as other Laravel server logs
Note that in Laravel 5, you have app/Exceptions/Handler.php as entry point for customize exception handling/reporting
https://laravel.com/docs/5.7/errors#the-exception-handler

Categories