This article explores how to harmonize internal logic and generated outputs in a HTTP context (typically a RESTful API) when it comes to handling errors.
Basically, there are three kinds of errors:
coding error: mistyped or inconsistent code, raising a compilation or parsing error
(under production environment, this should never occur)
logic error: the code does not act according to the underlying logic
(most of the time, no error nor warning is raised but unexpected result is returned)
use case error
- algorithm do not handle certain situations that lead to code misbehaviour;
- user provided invalid parameters (wrong configuration; mistyped, misformatted, undefined, or unexpected value).
Usually, reporting consists of logging or displaying messages that belong to one of those five families:
Extra information for tracing and debugging purpose.
Information about something that could indicate an error (or could turn into an error in the future), but could also happen in the normal course of the script execution.
Examples:
Alert about unexpected but not harming situation.
Examples:
Alert about something that prevented the request to be (fully) processed accordingly to the logic, and that might lead to unwanted behaviour and/or partial result.
Examples:
Information about a situation that could not be recovered from
Examples:
Sometimes, the distinction between recoverable-errors and fatal-errors is a bit ambiguous. When it comes to fatal-errors, a distinction should be made between "system code" and "user code".
Indeed, some errors (E_ERROR
, E_CORE_ERROR
, E_COMPILE_ERROR
, E_PARSE
) can be raised before the script is given the chance to customise how to handle these. In such scenario, we just want the processing to :
But other situations, when something makes the processing of the request impossible, can as well be considered as "fatal-errors" (authentication failure, missing mandatory data, invalid URI, ...). In those cases, processing should not be halted but rather send an appropriate response containing some information about the current error(s).
As there might be a lot of output, logging debug messages comes with an additional I/O cost. So, most of the time, it can be useful to :
The questions we want to address here are :
While keeping in mind that :
://stdout
We can take advantage of the internal PHP reporting mechanism :
// disable output to ://stdout
ini_set('display_errors', 0);
// ask for raw text messages
ini_set('html_errors', false);
// output fatal-errors messages to a custom (system) error log
ini_set('error_log', LOG_STORAGE_DIR.'/error.log');
// request reporting for all error levels
error_reporting(E_ALL);
Besides:
E_USER_*
constants, we drop general and deprecation notices and consider them as warnings):E_ERROR
: QN_REPORT_FATAL
E_USER_ERROR
: QN_REPORT_ERROR
E_USER_WARNING
: QN_REPORT_WARNING
E_USER_NOTICE
: QN_REPORT_DEBUG
QN_REPORT_FATAL
) inside user-code can be raised by throwing exceptions (when not caught, PHP default behaviour is to stop current script execution).ErrorReporter
provider using a distinct file for reporting: LOG_STORAGE_DIR.'/qn_error.log'
.ErrorReporter
is a simple Singleton with no dependencies, which hijack PHP default Error and Exception handlers:
public function __construct(/* no dependencies */) {
// assign a unique thread ID (using a hash apache pid and current unix time)
$this->setThreadId(md5(getmypid().microtime()));
set_error_handler(__NAMESPACE__."\Reporter::errorHandler");
set_exception_handler(__NAMESPACE__."\Reporter::uncaughtExceptionHandler");
}
And provides 4 main methods :
public function fatal($msg) {
$this->log(QN_REPORT_FATAL, $msg, self::getTrace());
die();
}
public function error($msg) {
$this->log(QN_REPORT_ERROR, $msg, self::getTrace());
}
public function warning($msg) {
$this->log(QN_REPORT_WARNING, $msg, self::getTrace());
}
public function debug($source, $msg) {
$this->log(QN_REPORT_DEBUG, $source.'::'.$msg, self::getTrace());
}
Here is an excerpt of the log
method:
private function log($code, $msg, $trace) {
// check reporting level
if($code <= error_reporting()) {
...
// append error message to log file
file_put_contents(LOG_STORAGE_DIR.'/qn_error.log', $error, FILE_APPEND);
}
}
In order to retrieve information about the current PHP stack, we use debug_backtrace
.
private static function getTrace($depth=0) {
// skip the reporter inner calls
$limit = 3+$depth;
$n = $limit-1;
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit);
// retrieve info from where the error was actually raised
$trace = $backtrace[$n-1];
...
return $trace;
}
That way, logging can be achieved accordingly to the above mentioned constraints, either by using the trigger_error
function or by calling the ErrorReporter
public methods:
/* debugging */
trigger_error('QN_SQL'.'sending SQL query: $query', QN_REPORT_DEBUG);
// equivalent to
$reporter->debug('QN_SQL', 'sending SQL query: $query');
/* warning */
trigger_error('Pay attention here', QN_REPORT_WARNING);
// equivalent to
$reporter->warning('Pay attention here');
/* recoverable error */
trigger_error('Something is wrong here', QN_REPORT_ERROR);
// equivalent to
$reporter->warning('Something is wrong here');
/* fatal error */
throw new Exception(QN_REPORT_FATAL, 'Things have gone really bad, stopping');
// equivalent to
$reporter->fatal('Things have gone really bad, stopping');
To improve chances that unhandled situations never occur, here are a few strategies: