XDXA

Blog / CakePHP's error handling

CakePHP's error handling

Whenever possible, I like to trigger the ap­pro­pri­ate HTTP status code for an error. CakePHP has a mechanism to throw 404 errors by simply calling:

<?php
$this->cakeError('error404');

This is handy when a request to a view action passes an id that doesn't ac­tu­al­lyex­ist. However, what about 403 or 500 errors? If you're like me, you probably stumbled upon AppError in the CakePHP book.

I defined a simple error403 method, and used it where ap­pro­pri­ate throughout my ap­pli­ca­tion. But when I rolled out to production, every 403 became a 404! Upon a bit of digging around in the code, I found the root of the problem in the Er­rorHan­dler con­struc­tor. In version 1.2.9, you can find this in cake/libs/error.php:

<?php
if ($method !== 'error') {
  if (Configure::read() == 0) {
    $method = 'error404';
    if (isset($code) && $code == 500) {
      $method = 'error500';
    }
  }
}

(Side note: Although in production error500 tech­ni­cal­ly can be called, $code doesn't appear to be declared in the con­struc­tors scope. Also, the method error500 isn't actually defined, which adds to my confusion.)

In production, all methods translate to either 404 or 500 errors! Now let me note, this does not appear to be mentioned anywhere in the book... So, how do we resolve this? Well, I found someone having a similar problem.

Teknoid sets debug to 1 when the error handler con­struc­tor is called. This causes the error handler to believe it's in debug mode and run whatever error method is passed its way.

But, there's a very important dis­tinc­tion. Teknoid overrides his _out­putMes­sage method to send an email to himself, not to display errors to users. If you were to simply set debug to 1 in the con­struc­tor, all CakePHP errors would be displayed to the user. This is why only error404 is allowed in vanilla CakePHP.

To resolve this, I created a white-list of allowed error handlers. When these error handlers are requested, I enable debug mode.

<?php
class AppError extends ErrorHandler {

  var $allowedMethods = array(
    'error403',
    'error500',
  );

  function __construct($method, $messages) {
    if (in_array($method, $this->allowedMethods)) {
      Configure::write('debug', 1);
    }

    parent::__construct($method, $messages);
  }

  function error403() {
    // I do some logging here to audit
    // who made the forbidden request

    header("HTTP/1.1 403 Forbidden");
    $this->controller->set(array(
      'name'  => __('403 Forbidden', true),
      'title' => __('403 Forbidden', true),
    ));

    $this->_outputMessage('error403');
  }
  ... error500 and so on ...
}

This is less than ideal, because turning on debug may have other im­pli­ca­tions throughout your ap­pli­ca­tion. We could reim­ple­ment (or copy and paste) the con­struc­tor with al­lowed­Meth­ods in mind, but that feels even more hack to me.

« Asus P4C800 Rev 2 Acrylamid »