How to catch and report fatal PHP errors

Discussion in 'General Linux HOWTOs' started by khiltd, Oct 22, 2007.

  1. khiltd

    khiltd New Member

    Nobody wants production code spewing potential security hole advertisements all over their pages, so it's generally considered good practice to define your own error handler--which can send you an email or an IM whenever something breaks--and turn PHP's error reporting functionality off entirely. PHP provides a very simple mechanism for accomplishing this, but unfortunately, it doesn't give you a hook to catch fatal errors caused by invalid code.

    I've seen a lot of crazy people come up with a lot of crazy solutions to this problem, but most have been unimaginative and needlessly complicated and I have no idea why they proliferate so wildly when simpler solutions are so readily possible with output buffers and a bit of ini_set() trickery. Bits and pieces of this solution can be found sprinkled about in the efforts of many others, but none of them ever seem to put the parts together as well as they could. I'm sure someone else must have figured it all out somewhere, but I've never seen it, and neither have most PHP developers I've encountered, so I'm posting it up somewhere Google will index someday in the hopes that the resultant karma will eventually catch up with me and I'll have less messy code coming in for repair.

    In order for this to work, you'll need to execute some startup code BEFORE anything else happens in your scripts, including any output buffering. This can be readily accomplished by adding the startup code to your auto_prepend_file as defined in php.ini. If you don't have an auto_prepend_file, make one. It's one of the most powerful and most overlooked features PHP has to offer.

    Code:
    //This may seem counterintuitive since our goal is to
    //hide errors from users, but our output buffer callback will
    //need to see everything in order to catch the bad stuff.
    ini_set('display_errors', 'On');
    
    //Use the normal mechanism for catching non-fatal errors.
    //This process is very well documented, so the definition
    //of this error handler function's body is left as an 
    //exercise for the reader. 
    set_error_handler('regular_error_handler');
    
    //Define some bogus, invalid HTML tags that no sane
    //person would ever put in an actual document and tell
    //PHP to delimit fatal error warnings with them.
    ini_set('error_prepend_string', '<phpfatalerror>');
    ini_set('error_append_string', '</phpfatalerror>');
    
    //Start an output buffer with a filter callback
    ob_start('fatal_error_handler');
    
    //Skim the buffer for our bogus HTML tags. If we find any
    //then we know a fatal error has occurred and should react accordingly.
    //It doesn't matter how the error string is formatted or if our page
    //contains similar looking text because we're looking for our own 
    //funky tags exclusively; false positives are highly improbable.
    function fatal_error_handler($bufferContents)
    {	
    	$output		= $bufferContents;
    	$matches	= array();
    	$errors		= "";
    	
    	if ( preg_match('|<phpfatalerror>.*</phpfatalerror>|s', $output, &$matches) )
    	{
    		foreach ($matches as $match)
    		{
    			$errors .= strip_tags($match) . "\n\n---\n\n";
    		}
    		
    		//Do your error logging/emailing/AIMing here 
    		//and overwrite $output so errors are not displayed.
    	}
    	
    	return $output;
    }
    You will absolutely take a measurable performance hit by running a preg_match over the output of every single page, but in my experience the difference is imperceptible under normal conditions. Still, you may want to define a global "off switch" to disable this functionality for scripts that don't need it, or for those times when you're debugging something and would prefer to see the default error spewage right there on the page.
     
  2. Tyco

    Tyco New Member

    Thank you very much! This saved my day(s).

    In fact, I took up that idea and created a kind of a general error handling class for handling PHP errors, PHP fatals errors and self-defined errors (eg. "wrong password" -> log to file) which I would like to share.

    Documentation is written in German, but I'll explain the basics here.


    For using this classes you just have to include one file BEFORE any output (ideally it is the first file that's included. (Again, as khiltd stated, auto_prepend_file would be a nice option for this)

    Code:
    // this will include all classes with correct dependancies AND register the error handler
    include('_include.php');
    
    After that you just have to register error handling Events with the ErrorObserver class. Sounds more complicated than it is:

    Code:
    // register a function that is called for ALL error types
    ErrorObserver::registerEvent('callbackfunction');
    
    // register a function that is called for PHP warnings only
    ErrorObserver::registerEvent('callbackfunction', ErrorType::PHP_E_WARNING);
    
    // register a function that is called for multiple error types
    ErrorObserver::registerEvent('callbackfunction', ErrorType::PHP_E_WARNING, ErrorType::PHP_E_ERROR, ErrorType::GENERAL_ERROR);
    

    I also built a class named "Event" that has a "trigger" method that can be ... triggered by an error event. To get a picture, how this is done, just look at one of the four error handling Event classes, I built.
    (ErrorFileLogEvent.php, ErrorShowOnScreenEvent.php, ErrorXmlLogEvent.php, ErrorMysqlDatabaseLogEvent.php)

    The $params Parameter given by the ErrorObserver class is an "Error" object, that contains valuable information for finding and debugging the error that occurred.
    With it comes a log of all globals variables that existed at the time, the complete backtrace generated by debug_backtrace(), the error type (one of the ErrorType::Constants or a self-defined error type) and the location where the error occurred (file and line number).


    Below, I post a larger example of how you could possibly use this thing.

    Code:
    <?php
    include ('_include.php');
    
    function log_it ($params = null)
    {
        file_put_contents('/var/log/mywebsite/error.log', $params->getMessage());
    }
    
    // will be triggered for all known error types
    ErrorObserver::registerEvents('log_it');
    
    // log_it() will NOT be called for this Event 'MyError' for it wasn't a known error type when log_it was registered
    ErrorObserver::registerEvent(new MyOwnEvent(), 'MyError');
    
    // logging the most vicious errors into an XML file
    ErrorObserver::registerEvent(new ErrorXmlLogEvent('/var/log/mywebsite/heavyerrors.log', ErrorType::PHP_E_CORE_ERROR, ErrorType::GENERAL_FATAL_ERROR, ErrorType::PHP_E_ERROR);
    ?>
    
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <title>My very cool error-free website</title>
        </head>
        <body>
    <?php
    1/0;    // will call log_it
    
    // will call MyOwnEvent->trigger()
    ErrorObserver::raise('I triggered this error on purpose', 'insert funny comment here', 'MyError');
    
    // this is a fatal error, so the calls are: log_it() and ErrorXmlLogEvent->trigger()
    SomeClass::SomeClass();
    ?>
    </html>
    


    PS: Don't forget that you can't use any echos within the ErrorObserver::handlePhpFatalError class method because it's an output buffer handler. I personally just use it to log the error and redirect the user with
    Code:
    header('Location: somewhereelse.php');
    This will ALWAYS work, because you are buffering the output, remember? ;)


    PPS: There is a problem I found with catching fatals. I tried logging, using relative paths to the logfile, which didn't work. It won't raise an error though, but it's kinda stupid, because we want to log it.
    However, if you use absolut paths to your logfile, there's no problem. (Or you could turn catching fatals off; just set ErrorObserver::$CATCH_PHP_FATAL_ERRORS = false, which should give a good performance boost too.
     

    Attached Files:

Share This Page