← Pervasive Code | Login Page →
In a live environment, no internal error should ever be displayed to the user, for two reasons:
- The default PHP errors look sloppy and any custom developer-oriented error messages are filled with overwhelming technical details. It’s always better to redirect the user to a clean “Sorry!” page.
- Any additional information beyond a basic “Sorry!” can tell an attacker things about your code (in severe cases, it might reveal the call stack and even some pieces of code). With a little more effort, the attacker could use that information to do more than just display an error.
On the other hand, in a development environment, displaying errors as they happen is good for the developers, who can correct them right away. So, an ideal error handling strategy would be to toggle direct display in a development environment, and logging to a file in a production environment.
I will be using a strategy that I would call “write and redirect”. In development mode, a complete error description is written in “files/cache/error-{unique-id}.txt” and the developer would be redirected to that page. This has the obvious benefit that the error is permanently logged and available at a certain address. In a live environment, the complete description is written instead in “logs/error-{unique-id}.txt” and the user is redirected to “http://domain/sorry“. As an additional improvement, you can mail the administrator whenever such an error happens, though I will not cover that here.
All of that can be toggled with a single boolean element in a configuration class, ErrorConfig.
<?php // config/error.php class ErrorConfig { // Is the "developer mode" enabled? public static function DeveloperMode() { return true; }// Where do we redirect live site users?public static function SorryPageUrl() { return DomainConfig::Url('/sorry'); }}
Of course, I will not hardcode the domain name in the error configuration file (that would be nasty), so I define it in a domain configuration file instead:
<?php // config/domain.php class DomainConfig {private static $domain = null; // The domain for this site public static function Domain() { if (DomainConfig::$domain === null) { DomainConfig::$domain = $_SERVER['HTTP_HOST']; if (!DomainConfig::$domain) DomainConfig::$domain = $_SERVER['SERVER_NAME'];if ($_SERVER['SERVER_PORT'] != '80') DomainConfig::$domain .= ':' . $_SERVER['SERVER_PORT'];} return DomainConfig::$domain; } private static $prefix = null; // The prefix for this site public static function Prefix() { if (DomainConfig::$prefix=== null) { if (array_key_exists('URL_PREFIX', $_ENV)) DomainConfig::$prefix = $_ENV['URL_PREFIX']; else DomainConfig::$prefix = ''; } return DomainConfig::$prefix; } // Transform an URI into an URL on this serverpublic static function Url($uri, $get = array()) { $url = "http://" . DomainConfig::Domain() . DomainConfig::Prefix() . $uri; $sep = '?'; foreach ($get as $key => $value) { if ($value === null) continue; $url .= $sep . urlencode($key) . '=' . urlencode($value); $sep = '&'; } return $url; }}
- If the link references “http://otherdomain/otherpage“, then I will go directly there. This is an absolute URL, and can only be interpreted in one way.
- If the link references “/otherpage“, I will go to “http://domain/otherpage“. This is an absolute URI, that deduces the domain part of the full URL by actually staying on the original domain.
- If the link references “otherpage“, I will go to “http://domain/page/otherpage“. This is a relative URI, that replaces the last segment of the current URL by itself.
The problem, in my grand scheme of things, is that I do not want to have control over what URL each link is relative to: whenever you take control of something, you either restrict the user or have to let the user configure your control. In this example, controlling the URL means:
- If the user wishes to move my software into a sub-directory of their website, such as “http://domain/jitbrain/“, then I cannot use absolute URIs (which would remove the jitbrain part of the URL) or I would have to let the user tell the program that there’s a prefix to be added.
- If the user wishes to point a DNS to his website (or work with my software on both a production website and on his own localhost), he will be visiting the same codebase through two domains, which lets me either adapt to multiple domain names or have the user change configuration files when he changes servers.
- Since the controllers are dictated only by the first important segment of the URI, it means that requests might have arbitrary “/arg/arg/arg” elements after the controller name. Using a relative URI in these circumstances would require me to count the arguments and backtrack the relative URI accordingly, which is annoying.
The solution I chose is to let the user configure access to the system in any way they see fit, including several ways at a time. The system will automatically know what domain name to use (simply by looking at the HTTP headers for the request and using the name used by the visitor), and it can be told what prefix to use in “/foo/bar” format by setting the URL_PREFIX environmentvariable in one of many ways (for instance, in an .htaccess file) with the default behavior being that there is no prefix.
See how the autoload function lets me define classes that refer to each other without having to manually include them?
Then comes the implementation of the error handler itself. Nothing too fancy, but it does the job:
<?php// utils/error.phpclass ErrorUtils { // Preliminary information private static function Prologue() { ob_start(); echo "[" . date('c') . "] " . $_SERVER['REQUEST_URI'] . "n"; } // Save output buffer to file and redirect private static function WriteAndRedirect() { echo "Stack contents:n"; debug_print_backtrace(); echo "Request data:n"; var_dump($_REQUEST); echo "Request info:n"; var_dump($_SERVER); $prefix = ErrorConfig::DeveloperMode() ? 'files/cache/error-' : 'log/error-'; $filename = $prefix . uniqid() . ".txt"; $redirect = ErrorConfig::DeveloperMode() ? DomainConfig::Url("/$filename") : ErrorConfig::SorryPageUrl();// Be windows-friendly$filename = str_replace( "/", DIRECTORY_SEPARATOR, $filename);// Always use the absolute path for file operations$root = dirname(dirname(realpath(__FILE__))); $filename = $root . DIRECTORY_SEPARATOR . $filename;@file_put_contents($filename, ob_get_clean());@header("HTTP/1.0 500 Internal Server Error");@header("Location: $redirect"); exit(0); } public static function UncaughtException($exception) { ErrorUtils::Prologue(); echo "Uncaught exception:n"; var_dump($exception); ErrorUtils::WriteAndRedirect(); } public static function AssertFailure() { ErrorUtils::Prologue(); echo "Assertion failure!n"; ErrorUtils::WriteAndRedirect(); } public static function Error($no, $str, $file, $line, $context) { ErrorUtils::Prologue(); echo "Level $no error '$str' at $file:$linen"; echo "Context:n" var_dump($context); ErrorUtils::WriteAndRedirect(); } }
In order to keep the code simple, the error message to be dumped into a file is constructed using echo, var_dump and various other constructs that print their result to the standard output. Since ErrorUtils::Prologue() enables output buffering and ErrorUtils::WriteAndRedirect() reads what was inside the output buffer, this ends up being a simple approach to constructing a text message.
Every error log includes, in order,
- the date and original URI,
- specific information (such as the exception object or error message),
- the stack backtrace with locations and argument values
- the full request data and metadata
This is the order one expects to do things in (when? what? where? why?).
The error handling system shown here is paranoid beyond control, since it will react to the tiniest warning it comes across. For instance, your program might be interrupted because you used the date() function without setting up a timezone. If that happens, blame your host, then simply set the date.timezone property in your php.ini. Or, in your .htaccess:
php_value date.timezone Europe/Paris
The proper solution, of course, is to avoid using the naked date function in your code for anything displayed to your users (what kind of sense does it make to display a time to an user that could be anywhere in the world?) but I’ll be coming back to that later.
← Pervasive Code | Login Page →
Hi. I'm Victor Nicollet,
0 Responses to “4. Error Handling”