Qua locus

Keeping both eyes on the long game.

User-friendly, developer-friendly error handling with Lithium


This blog has been split in three and all content moved to the new blogs – Qua Locus LifeQua Locus Tech and Qua Locus Puzzle – comments are disabled here.

Lithium’s error-handling infrastructure is very comprehensive, making it possible to handle errors in a rich and flexible way. However the documentation is still being fleshed out as the framework approaches version 1.0, so it’s not obvious yet how to take full advantage of it. In this post I describe the general error-handling features I’ve built for my web apps using Lithium’s error-handling infrastructure, including user-friendly error pages, distinguishing between different types of error, logging the error information to the database and also allowing users to contribute additional notes for some types of errors.

Configuring the ErrorHandler class is the first step. It can be done by editing config/bootstrap/errors.php and uncommenting the line which require’s this file in config/bootstrap.php. A simple errors.php file that differentiates between 404 errors and all other non-404 errors is:

use app\controllers\ErrlogsController;
use lithium\core\ErrorHandler;

$conditionsOther = array(
    'type' => 'Exception',
);

$conditions404 = array(
    'type' => 'Exception', // lithium\action\DispatchException for missing model, controller
                           // lithium\template\TemplateException for missing view
    'message' => "/(^Template not found|^Controller `\w+` not found|^Action `\w+` not found)/"
);

ErrorHandler::apply('lithium\action\Dispatcher::run', $conditionsOther, function($info, $params) {
    return ErrlogsController::handleOtherError($info, $params);
});

ErrorHandler::apply('lithium\action\Dispatcher::run', $conditions404, function($info, $params) {
    return ErrlogsController::handle404Error($info, $params);
});

ErrorHandler::run();

The non-404 error conditions are a superset of the 404 error conditions. This means that the non-404 conditions must be applied before the more specific 404 conditions, otherwise the non-404 (“other”) handler will handle all errors. The actual error handling is done in Errlogs and ErrlogsController, and anonymous functions are used here to call static methods in the error logs controller class. Finally, ErrorHandler::run is called. The default options for this method convert errors and notices into exceptions so that they can be caught by our Errlog code.

ErrlogsController is a standard Lithium Controller class, and handleOtherError and handle404Error are the same as standard controller actions (e.g. here), but static so that they can be easily referenced in errors.php.

Focusing on the non-404 errors as an example in this post, the controller’s handler method saves the error information to the database, then uses the ErrlogsController::renderErrorPage helper method to create a Response object with the rendered error page in it:

public static function handleOtherError($info, $params) {
    $userdata = Auth::check('member', $params['request']) ?: null;
    $errlogData = Errlogs::createOtherErrlogDataArray($info, $params, $userdata);
    $errlog = Errlogs::create($errlogData);
    $result = $errlog->save();

    return ErrlogsController::renderErrorPage(
        '500',
        array('errlog' => $errlog),
        $params['request'],
        $info['exception']
    );
}

protected static function renderErrorPage($template, $templateContent, $requestObj, $exceptionObj) {
    $response = new Response(array(
        'request' => $requestObj,
        'status' => $exceptionObj->getCode()
    ));

    Media::render($response, $templateContent, array(
        'library' => true,
        'controller' => 'errlogs',
        'template' => $template,
        'layout' => 'default',
        'request' => $requestObj
    ));

    return $response;
}

The code which assembles the error record for the database is in the Errlogs model, where Errlogs::assembleTrace is a static helper method that builds an easily readable stack trace from $info['exception']->getTrace() for storage into the DB:

    public static function createOtherErrlogDataArray($info, $params, $userdata = null) {
        $user_id = $userdata !== null ? $userdata['_id'] : null;

        $exceptionLogArray = array();
        $exceptionLogArray['message'] = $info['exception']->getMessage();
        $exceptionLogArray['file'] = $info['exception']->getFile();
        $exceptionLogArray['line'] = $info['exception']->getLine();
        $exceptionLogArray['trace'] = Errlogs::assembleTrace($info['exception']);

        $errlogData = array(
            'type' => 'other',
            'src' => $params['request']->referer(),
            'dest' => $params['request']->env('REQUEST_URI'),
            'user_id' => $user_id,
            'user_agent' => $params['request']->env('HTTP_USER_AGENT'),
            'exception' => $exceptionLogArray,
            'datetime' => new MongoDate()
        );
        return $errlogData;
    }

With this code so far, we can generate user-friendly error pages by defining some Errlogs views, and the error information is being logged to the database. We can also use different handlers for different types of error, rendering different views and storing different information in the DB for each. Extending this code so that users can contribute notes to errors is relatively straight-forward.

First, we include a form in the view which allows users to submit notes. Earlier, we assigned the errlog array as template content so that it was available as a variable in the view. Here, we reference it to get the _id of the error that we should associate any user note with. At the bottom of the view we use some jQuery code to submit any user note via AJAX and then display a nice thank you message.

Error page
<hr />
There has been a technical error, sorry! If you would like to email us with any
other feedback or information please use the form below. Thanks, Engineering
<div id="extraNote">
    <form id="extraNoteAdd" name="extraNoteAdd" enctype="multipart/form-data">
    <input id="extraNoteAdd_id" type="hidden" name="_id" value="<?= $errlog['_id']; ?>" />
    <textarea id="extraNoteAdd_note" name="note" rows="5" cols="60"></textarea>
    <input id="extraNoteButton" type="button" value="Add information" />
    </form>
</div>

<script type="text/javascript">// <![CDATA[
    $(document).ready(function() {
        $('#extraNoteButton').click(function() {
            var note = $('#extraNoteAdd_note').val();
            var errlog_id = $('#extraNoteAdd_id').val();
            $.ajax({
                type: 'POST',
                url: '/errlogs/ajax_add_note.json',
                data: { note: note, errlog_id: errlog_id },
                dataType: 'json',
                success: function(data, textStatus, jqHXR) {
                    $('div#extraNote').html('Thanks for sending us this extra information!');
                }
            });
        });
   });
// ]]></script>

Finally, the above view submits any user-contributed note to /errlogs/ajax_add_note using POST, so we create a matching action in ErrlogsController to handle this and update the DB:

public function ajax_add_note() {
    $conditions = array('_id' => $this->request->data['errlog_id']);
    $data = array('note' => $this->request->data['note']);
    Errlogs::update($data, $conditions);
    return array();
}

And that’s it. Any comments, suggestions and questions are very welcome!

Acknowledgements

It would not have been possible to develop this code without the Lithium documentation and a tutorial by Martin Samson (here). Special thanks also to Nate Abele for answering a StackOverflow question here which helped me get the paths to jQuery to be correct in the errlogs views.

Advertisements

Written by Christo Fogelberg

September 29, 02012 at 14:42

Posted in Tech

Tagged with , , ,

One Response

Subscribe to comments with RSS.

  1. Great write-up. One quick tip: a better way to filter specific exceptions is to use the ‘type’ key. For example, to replicate the above, without the regular expression match against the error message, you can do the following:

    ‘type’ => array(‘lithium\action\DispatchException’, ‘lithium\template\TemplateException’)

    Alternatively, since they both extend RuntimeException, you could also do:

    ‘type’ => ‘RuntimeException’

    …which will capture any RuntimeException, or any exception that extends it, which allows it to work on the same principle as a catch clause.

    Another useful one is the ‘stack’ filter, which lets you filter exceptions based on methods that appear in the call stack. For example, if you wanted to only intercept exceptions to read queries, you could add:

    ‘stack’ => ‘lithium\data\Model::find’

    Hope that helps. Keep up the good work.

    Nate Abele

    October 26, 02012 at 15:34


Comments are closed.

%d bloggers like this: