Rolling out an API for your cakePHP app Part 3: Handling Errors

by frank on April 30, 2010

in PHP

The last posts in this series covered the a basic architecture of an API in cakePHP, the limitations of that basic architecture and a possible solution using an API component and some external API classes. The last part is the handling of API specific errors. The goals for the error handling system:

  • Return specific error codes for API errors
  • Control over header response codes sent
  • Ability to version the responses
  • Return an error in the format of the initial request



We wanted to return custom error codes for particular API errors, this makes for a more simple and robust error reporting system especially if error messages will be localized at some stage. The inspiration for this came from the facebook and digg APIs.

Error Handling Class

To handle errors I use a custom error handling class written by primeminister, its best to start with his post and read through before continuing. I add a few methods to the app_error class to throw an error and pass extra parameters to primeminister’s error() method, and to render the API error using the correct version of the view.

<?php
App::import('Vendor', 'api/api');
class AppError extends ErrorHandler {
 
private $apiError = false;
 
 /**
 * HTTP response codes array
 *
 * @var array
 * @see http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 **/
var $codes = array(
    200 => 'OK',
    400 => 'Bad Request',
    401 => 'Unauthorized',
    402 => 'Payment Required',
    403 => 'Forbidden',
    404 => 'Not Found',
    405 => 'Method Not Allowed',
    406 => 'Not Acceptable',
    407 => 'Proxy Authentication Required',
    408 => 'Request Time-out',
    500 => 'Internal Server Error',
    501 => 'Not Implemented',
    502 => 'Bad Gateway',
    503 => 'Service Unavailable',
    504 => 'Gateway Time-out'
);
 
/**
 * Class constructor, overloading to make minor ammendment.
 *
 * @param string $method Method producing the error
 * @param array $messages Error messages
 */
function __construct($method, $messages) {
    App::import('Core', 'Sanitize');
    static $__previousError = null;
 
    if ($__previousError != array($method, $messages)) {
        $__previousError = array($method, $messages);
        $this->controller =& new CakeErrorController();
    } else {
        $this->controller =& new Controller();
        $this->controller->viewPath = 'errors';
    }
 
    $options = array('escape' => false);
    $messages = Sanitize::clean($messages, $options);
 
    if (!isset($messages[0])) {
        $messages = array($messages);
    }
 
    if (method_exists($this->controller, 'apperror')) {
        return $this->controller->appError($method, $messages);
    }
 
    if (!in_array(strtolower($method), array_map('strtolower', get_class_methods($this)))) {
        $method = 'error';
    }
 
    //Need to ammend below so that custom methods are triggered in production environments
    //if ($method !== 'error') {
    if (!in_array($method, get_class_methods('AppError'))) {
        if (Configure::read() == 0) {
            $method = 'error404';
            if (isset($code) && $code == 500) {
                $method = 'error500';
            }
        }
    }
    $this->dispatchMethod($method, $messages);
    $this->_stop();
}
 
/**
 * Error
 *
 * Handles the error with the right response code
 *
 * @param array $params
 * @return void
 * @author primeminister
 * @access public
 * @see http://apiwiki.twitter.com/REST+API+Documentation#HTTPStatusCodes
 */
public function error($params) {
 
    extract($params, EXTR_OVERWRITE);
    if (!isset($name)) {
        $name = $this->codes[$code];
    }
    if (!isset($url)) {
        $url = $this->controller->here;
    }
    // set header
    header("HTTP/1.x $code $name");
    $this->controller->set(array(
        'code'      => $code,
        'name'      => $name,
        'message'   => $message,
        'title'     => $code . ' ' . $name,
        'request'   => $url
    ));
 
 
    //If API error handle in a different way
    if ($this->apiError) {
        $this->controller->set(compact('apiErrorCode'));
    	$this->renderApiError();
    }
    else {
    	if ($this->controller->RequestHandler->isXml()) {
            $this->controller->RequestHandler->renderAs($this->controller, 'xml');
        }
 
        $this->_outputMessage('error');
    }
}
 
/**
 * Throw an API error, additionally passing an api error code.
 * 
 * @param Array $data Data sent by calling method, optionally includes apiErrorCode
 * 
 * @return void
 */
function apiError($data) {
 
    $this->apiError = true;
 
    //Get correct message and code based on API error code
    $apiErrorCode = 500;
    if (isset($data['apiErrorCode'])) {
    	$apiErrorCode = $data['apiErrorCode'];
    }
 
    $api = new Api();
    $errorData = $api->getError($apiErrorCode);
 
    $code = (isset($errorData['httpCode']))?$errorData['httpCode']:500;
 
    $message = __('API error.', true);
    if (isset($errorData['errorMessage'])) {
    	$message = $errorData['errorMessage'];
    }
    elseif (in_array($apiErrorCode, array_keys($this->codes))) {
    	$message = $this->codes[$apiErrorCode];
    }
 
    $this->error(array('code'=>$code,'message'=>$message, 'apiErrorCode'=>$apiErrorCode));
}
 
/**
 * Render an API error using correct error view based on current API version.
 * 
 * @return void
 */
function renderApiError() {
 
    //Retrieve the correct view
    $version = '0.3';
    $folder = $this->controller->RequestHandler->prefers();
    $view = 'error';
 
    if (isset($this->controller->params['version'])) {
    	$version = $this->controller->params['version'];
    }
 
    if ($this->controller->RequestHandler->isXml()) {
        $this->controller->RequestHandler->renderAs($this->controller, 'xml');
    }
 
    $errorFile = new File(VIEWS . "/errors/api/$version/$folder/error.ctp");
    if ($errorFile->exists()) {
        $view = "/errors/api/$version/$folder/error";
    }
    else {
 
        //Get error view for previous API version
        $version = substr(strstr($version, '.'), 1);
 
        //NOTE: again please note that the version of my API started at 0.3, you might need to alter the for loop below for your case
        for ($i = $version; $i >= 3; $i--) {
            $viewFile = new File(VIEWS . "/errors/api/0.$i/$folder/error.ctp");
            if ($viewFile->exists()) {
                $view = "/errors/api/0.$i/$folder/error";
                break;
            }
        }
    }
    $this->_outputMessage($view); 
}
}   
?>

The error codes and the method of retrieving those codes live in the API class:

    /**
     * Error codes for API errors
     */
    private $errorCodes = array(
        1001 => array('httpCode'=>500, 'errorMessage'=>'Version does not exist.'),
        1002 => array('httpCode'=>500, 'errorMessage'=>'Unknown output file.'),
        1003 => array('httpCode'=>500, 'errorMessage'=>'Unrecognised method.'),
    );
 
    /**
     * Retrieve data relating to an API error code
     * 
     * @param $errorCode Int API error code
     * @return Array Data associated with the error code.
     */
    public function getError($errorCode) {
        if (in_array($errorCode, array_keys($this->errorCodes))) {
        	return $this->errorCodes[$errorCode];
        }
    	return false;
    }

Throwing an error is now fairly straight forward:

$this->cakeError('apiError', array('apiErrorCode'=>1001));

The resulting response is a 500 error with the xml content:

<Error Code="1001" Request="/0.3/posts/get.xml">Version does not exist.</Error>

Error Response Views

In order to version the responses in accordance with the API version, the views are ordered into folders based on API version and response type. The view for the error above lives at: /views/errors/api/0.3/xml/error.ctp. The view itself:

<Error Code="<?php echo h($apiErrorCode) ?>" Request="<?php echo h($request) ?>"><?php echo h($message) ?></Error>

Final Word

That wraps up my API solution architecture. For the large part it accomplishes the goals I set for it and I’m happy with the result, feedback is always welcomed.

Was this article useful?

rss feed icon

Email this article to yourself or...

rss feed icon

Subscribe to the RSS feed for more useful articles and tips.

Share this article with others

  • del.icio.us
  • Twitter
  • Reddit
  • StumbleUpon
  • Facebook
  • Digg
  • Oliver

    Hi,

    I am really loving your approach and everything seems to run fine, except one thing..
    When there is a view file missing your app_error class is throwing an error:

    Notice (8): Undefined property: CakeErrorController::$RequestHandler [APP/app_error.php, line 165]

    Fatal error: Call to a member function prefers() on a non-object in /dev/app/app_error.php on line 165

    I tried a lot, but can’t find the mistake..

    Oliver

  • frank

    Hi Oliver, my guess is that you haven’t got the RequestHandler in your components array for that controller, try including the request handler at the top of the controller you are using when this error appears:
    var $components = array(‘RequestHandler’, ‘Api’);

  • http://www.deepinphp.com/2011/03/re-mobile-phone-layout-using-the-requesthandler-in-the-cakeerrorcontroller-apperror/ Re: Mobile Phone Layout – Using the RequestHandler in the CakeErrorController / AppError | DEEP in PHP

    [...] on March 10, 2011 by News Take a look at this link- [link] Basically it shows how to create a custom errorhandler including the ability to access the [...]