Exceptions and Error Handling

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:

  • Make every effort to recover from the situation;
  • Communicate clearly to the client that there was an error, and who is at fault (them or the server);
  • Provide detailed information about the error to the developer.

UserFrosting provides a framework for this process using custom PHP exceptions and exception handlers.

The exception lifecycle

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.

Exception handlers

Every custom exception handler implements five public methods (as defined by the ExceptionHandlerInterface):

Interface

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.

Default implementation

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).

Registering custom exception handlers

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 (\)!

Error renderers

To contribute to this documentation, please submit a pull request to our learn repository.

Debugging modes

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.

settings.displayErrorDetails

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.

site.debug.ajax

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 and site.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.

Custom exceptions

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.

HttpException

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;

Handling child exception types

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.