CakePHP's error handling
Whenever possible, I like to trigger the appropriate 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 actuallyexist. 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 appropriate throughout my application. 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 ErrorHandler constructor. 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 technically can be called, $code doesn't appear to be declared in the constructors 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 constructor 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 distinction. Teknoid overrides his _outputMessage method to send an email to himself, not to display errors to users. If you were to simply set debug to 1 in the constructor, 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 implications throughout your application. We could reimplement (or copy and paste) the constructor with allowedMethods in mind, but that feels even more hack to me.