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');
      }
  }
exclamationNote the distinction between URL and URI. Here, the URL is “http://domain/sorry” and the URI is just “/sorry“. Here, we need the full URL because we will use this information to perform a temporary HTTP redirect, and the HTTP protocol requires an URL to be provided.

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 server
      public 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;
      }
  }
exclamationI could have considered writing return “http://domain$uri”; instead of all this mumbo-jumbo, but that would have reduced the usability of the final product. One reason is the way in which the browser handles addresses. Suppose I am visiting “http://domain/page/subpage“, and I follow a link:

  • 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.php

  class 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,

  1. the date and original URI,
  2. specific information (such as the exception object or error message),
  3. the stack backtrace with locations and argument values
  4. the full request data and metadata

This is the order one expects to do things in (when? what? where? why?).

exclamationInstead of relying on a relative path for writing the error log, I’ve explicitly converted the original, relative path to an absolute path. Relative paths are evil, because they assume you know what they are relative to, which is sometimes not the case when you’re writing the code and often not the case when you’re reading it. Explicitly making relative paths absolute, and manipulating only absolute paths, alleviates this issue.

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

0 Responses to “4. Error Handling”


  1. No Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>



1150 feed subscribers
(readers who polled a feed this week)