Ajax error handling in CakePHP Ajax error handling in CakePHP ajax ajax

Ajax error handling in CakePHP


As mentioned above, Exceptions are the way to return an error on an AJAX request in CakePHP. Here is my solution for gaining finer control of what the error looks like. Also, as above, I am using a custom Exception Renderer, but not a custom exception. The default error response is a JSON object like this:

{"name":"An Internal Error Has Occurred", "url": "\/users\/login.json"}

I almost like the way that the default renderer handles AJAX errors; I just want to tweak it a little:

<?php// File: /app/Lib/Error/CustomExceptionRenderer.phpApp::uses('ExceptionRenderer', 'Error');class CustomExceptionRenderer extends ExceptionRenderer {    // override    public function error400($error) {        $this->_prepareView($error, 'Not Found');        $this->controller->response->statusCode($error->getCode());        $this->_outputMessage('error400');    }    // override    public function error500($error) {        $this->_prepareView($error, 'An Internal Error Has Ocurred.');        $code = ($error->getCode() > 500 && $error->getCode() < 506) ? $error->getCode() : 500;        $this->controller->response->statusCode($code);        $this->_outputMessage('error500');    }    private function _prepareView($error, $genericMessage) {        $message = $error->getMessage();        if(!Configure::read('debug') && !Configure::read('detailed_exceptions')) {            $message = __d('cake', $genericMessage);        }        $url = $this->controller->request->here();        $renderVars = array(            'name' => h($message),            'url' => h($url),            );        if(isset($this->controller->viewVars['csrf_token'])) {            $renderVars['csrf_token'] = $this->controller->viewVars['csrf_token'];        }        $renderVars['_serialize'] = array_keys($renderVars);        $this->controller->set($renderVars);    }}

Then, in bootstrap.php:

Configure::write('Exception.renderer', 'CustomExceptionRenderer');

So here is how it works:

  • Say I want to return a new CSRF token in my error response, so that if my existing token has been expired before the exception was thrown, I don't get blackholed the next time I try the request. Check out the Security Component documentation for more on CSRF protection.
  • Create a new class in app/Lib/Error. You can extend the default renderer, or not. Since I just want to change a few small things, and to keep the example simple, I'm extending it.
  • Override the methods that the default renderer uses to create the JSON object that will be returned. This is done with via the Request Handler Component, and conforms to best practices. Indeed, the default renderer does the same thing.
  • New private method to keep things DRY.
  • My solution to the problem of not getting custom error messages in production is to add an optional configuration key. By default this class will show the generic messages in production, but if you have debug set to 0, and you want the specific error messages: Configure::write('detailed_exceptions', 1);
  • Add the new token to the response if it exists. In my case, I have already called Controller::set on the new token in the beforeFilter method of AppController, so it is available in $this->controller->viewVars. There are probably dozens of other ways of accomplishing this.

Now your response looks like this:

{    "name":"The request has been black-holed",    "url":"\/users\/login.json",    "csrf_token":"1279f22f9148b6ff30467abaa06d83491c38e940"}

Any additional data, of any type can be added to the array passed to Controller::set for the same result.


I have also struggled with custom exceptions and error codes when using ajax requests (jquery mobile in my case). Here is the solution I came up with, without involving overwriting the debug mode. It throws custom errors in development mode, and also optionally in production mode. I hope it helps someone:

AppExceptionRenderer.php:

<?phpApp::uses('ExceptionRenderer', 'Error');class AppExceptionRenderer extends ExceptionRenderer {    public function test($error)     {           $this->_sendAjaxError($error);    }    private function _sendAjaxError($error)    {        //only allow ajax requests and only send response if debug is on        if ($this->controller->request->is('ajax') && Configure::read('debug') > 0)        {            $this->controller->response->statusCode(500);            $response['errorCode'] = $error->getCode();            $response['errorMessage'] = $error->getMessage();            $this->controller->set(compact('response'));            $this->controller->layout = false;            $this->_outputMessage('errorjson');        }    }}

You can leave out Configure::read('debug') > 0 if you want to display the exception in debug mode. The view errorjson.ctp is located in 'Error/errorjson.ctp':

<?phpecho json_encode($response);?>

In this case my exception is called

TestException

and is defined as follows:

<?phpclass TestException extends CakeException {    protected $_messageTemplate = 'Seems that %s is missing.';    public function __construct($message = null, $code = 2) {         if (empty($message)) {                     $message = 'My custom exception.';           }           parent::__construct($message, $code);    }}

Where I have a custom error code 2, $code = 2, for my json response. The ajax response will cast an error 500 with following json data:

{"errorCode":"2","errorMessage":"My custom exception."}

Obviously, you also need to throw the exception from your controller:

throw new TestException();

and include the exception renderer http://book.cakephp.org/2.0/en/development/exceptions.html#using-a-custom-renderer-with-exception-renderer-to-handle-application-exceptions

This may be a bit out of scope, but to handle the ajax error response in JQuery I use:

$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {    //deal with my json error});


You can use CakeExceptions as explained in the Cookbook: http://book.cakephp.org/2.0/en/development/exceptions.html BUT if you want to use your custom messages I found no other way than using debug = 1 in your production mode :(

Here's my approach with built in methods:

In your controller:

if($this->request->is('ajax')){    Configure::write('debug', 1);}if(!$allowed) {    throw new InternalErrorException('Keep your fingers away from me!'); // 500 error}

Change the error template to output nothing but the error when used in AJAX calls in /app/View/Errors/error500.ctp:

<?phpif($this->request->is('ajax')):    // Output for AJAX calls    echo $name;else:    //Standard CakePHP output ?>    <h2><?php echo $name; ?></h2>    <p class="error">        <strong><?php echo __d('cake', 'Error'); ?>: </strong>        <?php echo __d('cake', 'An Internal Error Has Occurred.'); ?>    </p>    <?php    if (Configure::read('debug') > 0 ):        echo $this->element('exception_stack_trace');    endif;endif; ?>

You can then parse the returned text in your AJAX. Here's the jQuery parts I use:

//...error: function (request) {    yourErrorShowingFunction(_this, request.responseText);}//...

Hope this helps :)

If anyone has an idea how to use the custom errors in production mode (without overwriting the debug mode) I would be very happy!