Things don't always go the way they were intended in your application. Sometimes this is the client's fault, and sometimes it is the server's (i.e. your) fault. When this happens, it is important that your application:
UserFrosting provides a framework for this process using custom PHP exceptions and exception handlers.
Any time an uncaught exception is thrown in your code, the underlying Slim application catches it and invokes a custom error handler. This may be familiar to you if you've used Slim before.
The difference with UserFrosting is that it replaces Slim's default error handler with a custom error handler, UserFrosting\Sprinkle\Core\Error\ExceptionHandlerManager
. ExceptionHandlerManager
receives the exception, along with the current Request
and Response
objects. Rather than handling the exception directly, though, ExceptionHandlerManager
checks to see if the exception type has been registered with a custom exception handler. If so, it invokes the corresponding exception handler; otherwise, it invokes the default UserFrosting\Sprinkle\Core\Error\Handler\ExceptionHandler
.
Every custom exception handler implements five public methods (as defined by the ExceptionHandlerInterface
):
handle()
The entry point for handling the exception. This is where you decide if/how to show an error message, debugging page, log the error, and/or do something different entirely;
renderDebugResponse()
Return an HTTP response containing detailed debugging information. The response body is generated by an ErrorRenderer
class.
renderGenericResponse()
Return an HTTP response containing a generic, user-friendly response without sensitive debugging information.
writeToErrorLog()
Write detailed error information and stack trace to the UserFrosting error log service (which defaults to writing to the logs/userfrosting.log
file).
writeAlerts()
Write user-friendly error messages to the alert message stream.
The basic UserFrosting\Sprinkle\Core\Error\Handler\ExceptionHandler
class already implements a default version of each of these methods. By default, it will render a debug response (renderDebugResponse
) when settings.displayErrorDetails
is set to true
. When displayErrorDetails
is set to false, it will instead log the error and stack trace, and display a generic response (renderGenericResponse
) to the client instead. If the error was generated during an AJAX request and AJAX debugging is disabled, it will write user-friendly error-messages to the alert stream as well.
In addition to the five public methods, you can also override protected methods in ExceptionHandler
to customize other behaviors. Take a look at the source code for ExceptionHandler
for more details.
As a basic example, lets take a look at the AuthExpiredExceptionHandler
class, which handles the AuthExpiredException
generated when an unauthenticated user attempts to access a protected resource:
<?php
namespace UserFrosting\Sprinkle\Account\Error\Handler;
use UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler;
/**
* Handler for AuthExpiredExceptions.
*
* Forwards the user to the login page when their session has expired.
* @author Alex Weissman (https://alexanderweissman.com)
*/
class AuthExpiredExceptionHandler extends HttpExceptionHandler
{
/**
* Custom handling for requests that did not pass authentication.
*/
public function handle()
{
// For auth expired exceptions, we always add messages to the alert stream.
$this->writeAlerts();
$response = $this->response;
// For non-AJAX requests, we forward the user to the login page.
if (!$this->request->isXhr()) {
$uri = $this->request->getUri();
$path = $uri->getPath();
$query = $uri->getQuery();
$fragment = $uri->getFragment();
$path = $path
. ($query ? '?' . $query : '')
. ($fragment ? '#' . $fragment : '');
$loginPage = $this->ci->router->pathFor('login', [], [
'redirect' => $path
]);
$response = $response->withRedirect($loginPage);
}
return $response;
}
}
As you can see, this handler extends the HttpExceptionHandler
, which in turn extends the base ExceptionHandler
class. HttpExceptionHandler
modifies the base behavior of ExceptionHandler
by reading an error message and status code from the corresponding HttpException
(or child subtype) and using these to construct the response.
Our AuthExpiredExceptionHandler
has overridden the default handle()
method to always write the client-friendly error message to the alert stream. In the case of non-AJAX requests, it also forwards the user to the login page, appending a redirect
parameter to the URL so that they will be automatically redirected to the last page they were on when their session expired.
Notice that, no matter what, we never display a debugging page when handling an AuthExpiredException
. This is because AuthExpiredException
is not an error at all, but rather an exception that can be thrown during production when the application itself is functioning perfectly normally.
You should always have your custom exception handlers extend either the base
ExceptionHandler
class, or one of its child classes (HttpExceptionHandler
, etc).
Once you have defined your custom exception handler, you'll need to map the corresponding exception to it in your service provider.
To do this, simply extend the errorHandler
service and call the register
method on the $handler
object:
$container->extend('errorHandler', function ($handler, $c) {
// Register the MissingOwlExceptionHandler
$handler->registerHandler('\UserFrosting\Sprinkle\Site\Database\Exceptions\MissingOwlException', '\UserFrosting\Sprinkle\Site\Error\Handler\MissingOwlExceptionHandler');
return $handler;
});
The first argument is the fully qualified name of the exception class, and the second argument is the fully qualified name of the handler class. Notice that we need to use the fully qualified names, including the entire namespace and a leading backslash (\
)!
To contribute to this documentation, please submit a pull request to our learn repository.
In UserFrosting 4.1, the decisions of how to render the response and what information to reveal is left up to the handler's implementation of handle()
. Nonetheless when writing your handle
implementation, you should take into account two configuration parameters: settings.displayErrorDetails
, and site.debug.ajax
.
When this setting is enabled, it indicates that the application is configured for "development mode", and that detailed debugging information can be sent in the response. In this case, you may want to invoke the renderDebugResponse
method. When this setting is disabled, it means that we do not want to display a debugging page - in fact, this could leak sensitive information to the client! Instead, you should invoke renderGenericResponse
and/or writeToErrorLog
.
Normally, when displayErrorDetails
is enabled and an error is generated during an AJAX request that requests a JSON response, the JsonRenderer
will be invoked to return a JSON object containing the debugging information.
However for some types of exceptions, you may wish to display a debugging page in the browser instead - even for AJAX requests! When this setting is enabled, ExceptionHandler
will ignore the requested content type and generate an HTML response instead. In client side code, when site.debug.ajax
is enabled and an error code is received by an AJAX call, your Javascript can use this information to decide whether or not to completely replace the current page with the HTML debug page that was returned in the response.
For example, in the ufAlerts
widget:
base._newMessagesPromise = $.getJSON( base.options.url )
.done(function ( data ) {
if (data) {
base.messages = $.merge(base.messages, data);
}
base.$T.trigger("fetch.ufAlerts");
}).fail(function ( data ) {
base.$T.trigger('error.ufAlerts');
if ((typeof site !== "undefined") && site.debug.ajax && data.responseText) {
document.write(data.responseText);
document.close();
} else {
if (base.options.DEBUG) {
console.log("Error (" + data.status + "): " + data.responseText );
}
}
});
You'll notice the block:
if ((typeof site !== "undefined") && site.debug.ajax && data.responseText) {
document.write(data.responseText);
document.close();
}
This lets you display an error report when an exception is thrown during the AJAX request to the /alerts
route.
By default, both
settings.displayErrorDetails
andsite.debug.ajax
are disabled in the production configuration environment. Do not change this! Displaying detailed exception traces in production is an extreme security risk and could leak sensitive passwords to your users and/or the public.
UserFrosting comes with the following exceptions already built-in:
RuntimeException (built-in to PHP)
├── UserFrosting\Support\FileNotFoundException
├── UserFrosting\Support\JsonException
└── UserFrosting\Sprinkle\Core\Throttle\ThrottlerException
UserFrosting\Support\HttpException
├── UserFrosting\Support\BadRequestException
├── UserFrosting\Support\ForbiddenException
├── UserFrosting\Sprinkle\Core\Model\DatabaseInvalidException
└── UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException
├── UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountDisabledException
├── UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountInvalidException
├── UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountNotVerifiedException
├── UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException
├── UserFrosting\Sprinkle\Account\Authenticate\Exception\InvalidCredentialsException
├── UserFrosting\Sprinkle\Account\Authorize\AuthorizationException
├── UserFrosting\Sprinkle\Account\Controller\Exception\SpammyRequestException
└── UserFrosting\Sprinkle\Account\Util\HashFailedException
You can define your own exceptions, of course, optionally inheriting from any of these existing exception types. Every exception you define must eventually inherit back to PHP's base Exception
class.
You'll notice that a large portion of UserFrosting's exception types inherit from the HttpException
class. This is an interesting exception (no pun intended) - it acknowledges that the exception message (which you would want your developers and sysadmins to see), and the client messages (which are displayed to the user to let them know that something went wrong), are generally different things.
The HttpException
class acts like a typical exception, but it maintains two additional parameters internally: a list of messages that the exception handler may display to the client, and a status code that should be returned with the response. As a simple example, consider the AccountInvalidException
:
<?php
namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
use UserFrosting\Support\Exception\HttpException;
class AccountInvalidException extends HttpException
{
protected $default_message = 'ACCOUNT.INVALID';
protected $http_error_code = 403;
}
It defines a default message, ACCOUNT.INVALID
, that the registered exception handler can display on an error page or push to the alert stream. It also sets a default HTTP status code to return with the error response.
The default message can be overridden when the exception is thrown in your code:
$e = new AccountInvalidException("This is the exception message that will be logged for the dev/sysadmin.");
$e->addUserMessage("This is a custom error message that will be sent back to the client. Hello, client!");
throw $e;
Internally, ExceptionHandlerManager
uses the instanceof
method to map a given exception to a given handler. This means that if your exception is an instanceof
multiple different classes, for example if you inherited from another exception class, then ExceptionHandlerManager
will use the handler mapped to the last matching exception type. For example, if you have an exception:
<?php
class MissingOwlException extends MissingBirdException
{
}
And you have registered a handler for MissingBirdException
:
$container->extend('errorHandler', function ($handler, $c) {
$handler->registerHandler('\UserFrosting\MissingBirdException', '\UserFrosting\MissingBirdExceptionHandler');
return $handler;
});
Since MissingOwlException
inherits from MissingBirdException
, UserFrosting will use the MissingBirdExceptionHandler
to handle your MissingOwlException
, unless later on you register another handler specifically on the MissingOwlException
type.