Symfony 5 custom 404 page - symfony

I'm trying to create a custom 404 page for a Symfony 5 project that must:
Output a simple JSON-encoded string, like "Not found".
Said string must be read from a translation resource.
Have an additional Content-Type: application/json header.
There is a section in the Symfony docs, that attempts to explain how this can be achieved, but the information seems incomplete/incorrect, apparently being written for the 4.X version, even pointing to non-existent source files on GitHub.
I have managed to create an error controller, but it swallows all errors:
# config/packages/framework.yaml
framework:
error_controller: App\Controller\ErrorController::errorHandler
// src/Controller/ErrorController.php
class ErrorController extends AbstractController
{
public function errorHandler(TranslatorInterface $translator) : JsonResponse
{
return new JsonResponse($translator->trans('not_found'));
}
}
The problem is that this results in any error (including internal ones) returning a 404 page.
How can I make this controller/method handle only 404 errors and leave everything else to be handled as before by the framework itself?

For anyone else that is looking for a solution to a JSON 404 page for a Symfony application:
I was looking for a way to use a controller to handle specific error cases as it seemed the easiest option on the surface, but this does not seem to be possible, or at least I have not figured out how.
In the end, I reached a solution using events and event listeners:
Configuration:
# config/services.yaml
services:
...
# This listener handles only 404 errors in PROD mode
App\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
Event listener:
// src/EventListener/ExceptionListener.php
class ExceptionListener {
public function onKernelException(ExceptionEvent $event) : void
{
if (
$_ENV['APP_ENV'] != 'prod'
|| !$event->isMasterRequest()
|| !$event->getThrowable() instanceof NotFoundHttpException
) {
return;
}
// Send a not found in JSON format
$event->setResponse(new JsonResponse($this->translator->trans('not_found')));
}
}

Related

Symfony: redirecting to homepage after encountering an error

I've recently started learning Symfony, and I've been trying to make an app that will redirect user to the homepage after encountering an error (For the sake of the question, it can be error 404) However, I had problems with finding a way to do so.
Before, I used TwigErrorRenderer as described in Symfony documentation to handle my errors, but it only explains how to redirect to new error pages created by myself. Could somebody help me with this issue?
It is generally not a good idea to do this, because you want to tell the user that their request was not processed due to an error, or that they accessed non-existing page.
But if you really want to, you can achieve it with this Event Listener.
// src/EventListener/ExceptionListener.php
<?php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\Routing\RouterInterface;
final class ExceptionListener
{
private RouterInterface $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function onKernelException(ExceptionEvent $event): void
{
// You should log the exception via Logger
// You can access exception object via $event->getThrowable();
$homepageRoute = $this->router->generate('homepage', [], RouterInterface::ABSOLUTE_URL);
$response = new RedirectResponse($homepageRoute);
$event->setResponse($response);
}
}
You also need to register the Event Listener in your services.yaml.
services:
App\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
Please note the following:
The Event Listener assumes that your Homepage route is called homepage;
you really should log the exception or you will lose logs about all of them;
as stated at the top of this answer, this is not a good approach to deal with exceptions.

Have all undefined routes return 404 in symfony 4

I'm new to Symfony, and I'm working on a first project, using Symfony 4.1. I have a home page configured at '/', and that's working. The point of this project is to create a REST API, and I have a route defined at /api/argument/, and that's working fine, too.
However, when I navigate to /api, or any other route with something in the path (e.g. /car, /apple, /something/else), I get a 404 in the HTTP response, but the page displays a backtrace of the exception.
Sorry, the page you are looking for could not be found.
NotFoundHttpException
No route found for "GET /oijoij"
in RouterListener.php line 139
at RouterListener->onKernelRequest(object(GetResponseEvent), 'kernel.request', object(EventDispatcher))in EventDispatcher.php line 212
...
So then in .env I set
APP_ENV=test
APP_DEBUG=0
And now, instead of a pretty-printed HTML backtrace, I get just a plain-text error message
Fatal error: Uncaught Symfony\Component\Routing\Exception\ResourceNotFoundException in www\project\var\cache\test\srcTestProjectContainerUrlMatcher.php:50 Stack trace: ...
How do I configure this so that undefined routes return a 404 with the 404 template page, without a backtrace?
I'm also throwing a BadRequestHttpException in my API controllers, and that exception just gets dumped on the page too. Does this get solved by the same method?
As many people have pointed out, the error pages are generated for the dev and test environment only. In the prod environment, which you should use on your live system, will display a generic 404 page, which you can customize.
The system responsible for showing either the detailed error page in development or the regular 404 page in production is Symfony's event cycle, more specifically the kernel.exception event, which is listened to and then any uncaught errors and exceptions will be converted into an error page-response. Since you are writing an API you might want to register your own listener and return a JSON response instead of regular HTML.
An event subscriber for this could look something like this:
<?php declare(strict_types = 1);
namespace App\Api\Response;
use Exception;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelEvents;
final class ExceptionToJsonResponseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::EXCEPTION => 'onKernelException',
];
}
public function onKernelException(GetResponseForExceptionEvent $event): void
{
// Skip if request is not an API-request
$request = $event->getRequest();
if (strpos($request->getPathInfo(), '/api') !== 0) {
return;
}
$exception = $event->getException();
$error = [
'type' => $this->getErrorTypeFromException($exception),
// Warning! Passing the exception message without checks is insecure.
// This will potentially leak sensitive information.
// Do not use this in production!
'message' => $exception->getMessage(),
];
$response = new JsonResponse($error, $this->getStatusCodeFromException($exception));
$event->setResponse($response);
}
private function getStatusCodeFromException(Exception $exception): int
{
if ($exception instanceof HttpException) {
return $exception->getStatusCode();
}
return 500;
}
private function getErrorTypeFromException(Exception $exception): string
{
$parts = explode('\\', get_class($exception));
return end($parts);
}
}
This will convert any exception into a JSON-response with a custom format similar to this:
{
"type": "NotFoundException",
"message": "Could not find argument with id x"
}
This listener will only do this for routes that start with /api so if you have both an API and a "regular" site it should not interfere with the default error handling.

Exclude all 404 errors from monolog

Currently I have monolog sending me emails when there are errors. I found that 404 errors were just polluting my email and my provider ended up suspending my account due to the number of emails I sent myself. I decided to exclude all 404 errors because all of them were due to bots looking for vulnerabilities and not from clients.
Exclude code:
excluded_404s:
- ^/
The problem I'm seeing now is that symfony still logs 404 errors if the bots use http methods other than GET. My email is now polluted with entries like
HEAD :80/phpmyAdmin/
How can I exclude all 404 errors including those using http methods other than GET?
Edit:
Oh boy. Beginner mistake here. It seems that after my last deploy of the configuration I did not clear the prod cache and I'm figuring out that the config is cached. I'm using deployer to deploy my code updates but I guess the clear cache command is missing from it.
Symfony 4.1. upwords can can be configured to ignore HTTP Codes:
config/packages/monolog.yaml
monolog:
handlers:
main:
# ...
type: 'fingers_crossed'
excluded_http_codes: [404]
https://symfony.com/blog/new-in-symfony-4-1-ignore-specific-http-codes-from-logs
I know it is an old question, but it is the 1st one when googling.
Meanwhile I found a solution for symfony 3.4, if you need to exclude all 404 errors from your logs you can create an ExceptionListener and check if the exception is a NotFoundHttpException.
If it is the case, then just return a response so that the event is stopped and the logger will not handle the exception.
src/AppBundle/EventListener/ExceptionListener.php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
if (! $exception instanceof NotFoundHttpException) {
return;
}
$response = new Response($exception->getMessage(), $exception->getStatusCode());
// returning a response stop the event propagation
$event->setResponse($response);
}
}
src/AppBundle/Resources/config/services.yml
services:
app.exception.listener:
class: AppBundle\EventListener\ExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception }
In Symfony 4/5 you can do:
<?php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ExceptionListener
{
public function onKernelException(ExceptionEvent $event)
{
$exception = $event->getThrowable();
if ($exception instanceof NotFoundHttpException) {
$response = new Response($exception->getMessage(), $exception->getStatusCode());
$event->setResponse($response);
}
return;
}
}
and
App\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }

Custom ExceptionListener picks up 403 & 404, but not 500 errors

Exact same problem as unanswered question Symfony 2.4: Why are 500 errors not caught by kernel.exception listener.
I have implemented a custom exception "handler" which works awesomely for 403 and 404 issues, but 500 errors (which is what I REALLY want to handle, as I want to send an email to myself from the system when this happens) does not trigger my custom "handler" and continues to behave as if the custom "handler" was not there. The code is relatively straight forward:
Extract from app/config/config.yml:
services:
core.exceptlistener:
class: Pmb\LicensingBundle\Listener\ExceptionListener
arguments: ["#service_container", "#router"]
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException, priority: 200 }
Entire \Pmb\LicensingBundle\Listener\ExceptionListener.php:
<?php
namespace Pmb\LicensingBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Bundle\TwigBundle\TwigEngine;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
class ExceptionListener
{
private $container;
private $router;
function __construct($container, $router)
{
$this->container = $container;
$this->router = $router;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
die("test");
$request = $this->container->get('request');
$templating = $this->container->get('templating');
if ($exception instanceof \Symfony\Component\Security\Core\Exception\AccessDeniedException)
{
# If AJAX request, do show not error page.
if ($request->isXmlHttpRequest())
{
$response = new Response(json_encode(array('error' => 'Access Denied: Not logged in as administrator')));
}
else
{
return new RedirectResponse($this->router->generate('login'));
}
$event->setResponse($response);
}
elseif ($exception->getStatusCode() == 404)
{
# If AJAX request, do show not error page.
if ($request->isXmlHttpRequest())
{
$response = new Response(json_encode(array('error' => 'Requested route could not be found')));
}
else
{
$response = new Response($templating->render('PmbLicensingBundle:Exception:error404.html.twig', array(
'exception' => $exception
)));
}
$event->setResponse($response);
}
else
{
# TODO: Send Email
# If AJAX request, do not show error page.
if ($request->isXmlHttpRequest()) $response = new Response(json_encode(array('error' => 'Internal Server Error Encountered. Developer has been notified.')));
else
{
$response = new Response($templating->render('PmbLicensingBundle:Exception:error500.html.twig', array(
'exception' => $exception
)));
}
}
}
}
I put the die("test") in there to verify that the "handler" is not being called at all and that is not just a problem with my if-else logic. As stated before, this works awesomely with any 404 or 403 errors, but 500 errors completely ignores this and behaves in the default manner. I am pretty sure that this has to do with the registering of the listener service, but I cannot find anything that explains how to make it work properly.
EDIT: Below are screenshots of errors that are not being handled as expected. I notice on the one (undefined variable) the die("test"); actually displays at the bottom, but on the ohter (syntax error) it does not display, even though both seems like they are being caught by Symfony2. Further testing showed that die("test"); showed up, but die(">> .$exception->getStatusCode()." <<"); did not. I am assuming that this is causing a second exception which I am not seeing, just like AccessDeniedException did not have this function call and I had to use instanceof, but there are many possible errors that can come up with a 500 error, so how do I distinguish whether the error is one of these?
FURTHER EDIT: On the error that did print the "Test" at the bottom, I noticed that when I do $container->testError();, I do not get the "Test", at the bottom of the error, but when I do return $container;, I do, even though both errors are ContextErrorException with undefined variable.
The 500 errors you got most probably before Symfony has chance to start. Since Symfony doesn't start in this case it cannot handle errors. To debug such cases, you should look at web server (apache, ngnix, etc) logs.
I just tested catching 500 exceptions and it seems to work fine. I think you may have something else intercepting the exception? Perhaps start a new project and try it again.
I myself prefer the subscriber interface:
class ExceptionListener extends ContainerAware implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(KernelEvents::EXCEPTION => array(
array('doException', 200),
));
}
public function doException(GetResponseForExceptionEvent $doEvent)
{
$exception = $doEvent->getException();
die('Exception caught ' . $exception->getStatusCode());
cerad_core__exception_listener:
class: Cerad\Bundle\CoreBundle\EventListener\ExceptionListener
tags:
- { name: kernel.event_subscriber }
Be aware that you can also configure the logger to send emails. Might be easier.
http://symfony.com/doc/current/cookbook/logging/monolog_email.html
#Ferhad makes a good point. Some (but not all) 500 errors will basically crash the S2 app. But I am assuming that you are getting the default S2 exception message.

Is there a symfony2 event/handler for session timeout a.k.a not logged in

Im using the FOSRestBundle to make ajax json calls within a Firewall. Everything seems to be working great, with the exception that Im not able to to handle when a session timeout has occurred. Right now it's redirecting to login_check in this scenario, returning html rather than json to the client.
Im aware, and use success_handler and failure_handler's within my app. I cannot find a built in handler for dealing with authorisation failures, such as session timeout.
Is there something within the FOSRestBundle that can help address this, or something Im not seeing within Symfony2?
Yes, Symfony offers the possibility to handle exceptions. You have to create an event listener which observes the kernel.exception event with a high priority. Create an event handler like this:
<?php
namespace Acme\Bundle\MyBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class AjaxAuthenticationListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
$request = $event->getRequest();
$format = $request->getRequestFormat();
$exception = $event->getException();
if ('json' !== $format || (!$exception instanceof AuthenticationException && !$exception instanceof AccessDeniedException)) {
return;
}
$response = new JsonResponse($this->translator->trans($exception->getMessage()), $exception->getCode());
$event->setResponse($response);
$event->stopPropagation();
}
}
Now you have to register your event handler in one of your service.yml's, like this:
kernel.listener.ajax_authentication_listener:
class: Acme\Bundle\MyBundle\EventListener\AjaxAuthenticationListener
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException, priority: 250 }
Note the priority parameter, used to tell Symfony to execute the handler before its own handlers, which have a lower priority.
And on your frontend you can register an event handler for jQuery, which reloads the page on such an error.
$(document).ready(function() {
$(document).ajaxError(function (event, jqXHR) {
if (403 === jqXHR.status) {
window.location.reload();
}
});
});
See this gist for reference.
I'm not sure if there's anything explicitly inside the FOSRest bundle, but Symfony2 itself is able to handle session timeouts.
Have you tried looking here? http://symfony.com/doc/current/components/http_foundation/session_configuration.html

Resources