Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ajax error handling in CakePHP

I want to do something very similar to this but in the CakePHP world for AJAX requests. At the moment I am doing this:

$this->autoRender = false;
$this->response->statusCode(500);

It is based off of this. However this solution does not allow me to include a custom message like in the Rails example, so that way, in my clients side error handler, I can display the message included in the 500 error response.

How would I implement the same functionality in CakePHP like the Ruby on Rails example?

like image 705
paul smith Avatar asked Jul 20 '12 19:07

paul smith


2 Answers

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.php
App::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.

like image 186
trey-jones Avatar answered Oct 23 '22 07:10

trey-jones


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:

<?php
App::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':

<?php
echo json_encode($response);
?>

In this case my exception is called

TestException

and is defined as follows:

<?php
class 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
});
like image 38
Yoggi Avatar answered Oct 23 '22 06:10

Yoggi