Blog

Blog: Mayday, Mayday, Mayday - PHP Going Down

PHP provides a variety of tools for handling errors and exceptions, in particular extensible handlers for managing errors and exceptions as they occur. However, there is a range of fatal errors that PHP does not directly provide tools for you to handle them. In the best of scenarios, these fatal errors will result in partially outputted pages, or the “white screen of death.”

These errors include:

E_ERROR
Fatal run-time errors. These indicate errors that can not be recovered from, such as a memory allocation problem. Execution of the script is halted
E_PARSE
Compile-time parse errors. Parse errors should only be generated by the parser
E_CORE_ERROR
Fatal errors that occur during PHP’s initial startup. This is like an E_ERROR, except it is generated by the core of PHP
E_COMPILE_ERROR
Fatal compile-time errors. This is like an E_ERROR, except it is generated by the Zend Scripting Engine.

By leveraging a combination of shutdown function, error_get_last, and output buffering it is possible to put a structure in place that will allow you to trap fatal errors. The theory of operation is quite simple:

  1. Register a shutdown function.
  2. When the shutdown function is called, check to see if the script finished executing as expected by interrogating error_get_last for any fatal errors.
  3. Handle the fatal error as you see fit.

Let’s look at some code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
 
ob_start();
 
$res = register_shutdown_function('shutdown');
 
function shutdown()
{
    if ($error = error_get_last()) {
        if (isset($error['type']) && ($error['type'] == E_ERROR || $error['type'] == E_PARSE || $error['type'] == E_COMPILE_ERROR)) {
            ob_end_clean();
 
            if (!headers_sent()) {
                header('HTTP/1.1 500 Internal Server Error');
            }
 
            echo '<h1>Bad Stuff Happend</h1>';
            echo '<p>But that is okay</p>';
            echo '<code>' . print_r($error, true) . '</code>';
        }
    }
}
 
ob_end_flush();
?>

The overall page generated is wrapped in an output buffer before being sent to the client. The use of output buffering here allows us to get rid of the partially generated content when the fatal error occurs and replace it with a custom error message. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
 
ob_start();
 
$res = register_shutdown_function('shutdown');
 
function shutdown()
{
    if ($error = error_get_last()) {
        if (isset($error['type']) && ($error['type'] == E_ERROR || $error['type'] == E_PARSE || $error['type'] == E_COMPILE_ERROR)) {
            ob_end_clean();
 
            if (!headers_sent()) {
                header('HTTP/1.1 500 Internal Server Error');
            }
 
            echo '<h1>Bad Stuff Happend</h1>';
            echo '<p>But that is okay</p>';
            echo '<code>' . print_r($error, true) . '</code>';
        }
    }
}
 
?>
 
<h1>My Bad Day</h1>
 
<?php require 'FileNotFound.php'; ?>
 
<p>What a bad day...</p>
 
<?php
 
ob_end_flush();
 
?>

The failed require call generates a fatal E_COMPILE_ERROR. We trap the error, and instead of telling the client about “my bad day” we retract that and give the user an ever so comforting message that all is okay even though our website blew up. Instead of doing something smart like logging the error, we display it:

1
2
3
4
5
6
7
8
<h1>Bad Stuff Happend</h1><p>But that is okay</p><code>Array
(
    [type] => 64
    [message] => require() [<a href='function.require'>function.require</a>]: Failed opening required 'FileNotFound.php' (include_path='.:/usr/local/lib/php')
    [file] => /home/michael/www/eggplant/public_html/samples/fatal/index.php
    [line] => 28
)
</code>

If we fix the missing required file by adding the following script:

1
2
3
<?php
i'm an idiot
?>

…we are able to catch the E_PARSE fatal error in our shutdown handler:

1
2
3
4
5
6
7
8
<h1>Bad Stuff Happend</h1><p>But that is okay</p><code>Array
(
    [type] => 4
    [message] => syntax error, unexpected T_STRING
    [file] => /home/michael/www/eggplant/public_html/samples/fatal/FileWithParseError.php
    [line] => 2
)
</code>

Let’s fix up that required file with some real code:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
 
myBadTypo();
 
function myBadDay()
{
    ini_set('memory_limit', '5000');
 
    echo str_repeat('bad bad bad', 50000);
}
 
?>

…Another caught E_ERROR:

1
2
3
4
5
6
7
8
<h1>Bad Stuff Happend</h1><p>But that is okay</p><code>Array
(
    [type] => 1
    [message] => Call to undefined function myBadTypo()
    [file] => /home/michael/www/eggplant/public_html/samples/fatal/FileThatBlowsMemLimits.php
    [line] => 3
)
</code>

Finally, let’s say we get the required file sorted out but blow the bank with available memory:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
 
myBadDay();
 
function myBadDay()
{
    ini_set('memory_limit', '5000');
 
    echo str_repeat('bad bad bad', 50000);
}
 
?>

…we are able to catch the E_ERROR fatal error in our shutdown handler too:

1
2
3
4
5
6
7
8
<h1>Bad Stuff Happend</h1><p>But that is okay</p><code>Array
(
    [type] => 1
    [message] => Allowed memory size of 262144 bytes exhausted (tried to allocate 550001 bytes)
    [file] => /home/michael/www/eggplant/public_html/samples/fatal/FileThatBlowsMemLimits.php
    [line] => 9
)
</code>

That’s all pretty good, however there are some limits that need to be taken into consideration:

  • E_PARSE errors on the include that builds out the shutdown structure will not be caught, as the interpreter can’t get beyond the parse error to put it into place.
  • E_CORE_ERROR errors? These are environmental errors caught when PHP itself is bootstrapping — pre-script interpretation. No dice.
  • When using output buffering in combination with the ob_gzhandler, take care when cleaning the buffer. Chances are the Content-Encoding: gzip header was already set and you will need to follow suit.
  • If your output buffer has already been flushed before hitting a fatal error and the shutdown function, your content to that point is already gone. Check ob_get_status to see if that is the case.

For an similar implementation using a shutdown function, check out eZ Components’ Execution class.

Tags:

9 Responses to “Mayday, Mayday, Mayday - PHP Going Down”

  1. Great article! A little addition: output handlers will be called on fatal errors too.

  2. Mayday, Mayday, Mayday - PHP Going Down…

    [...]PHP provides a variety of tools for handling errors. However, there is a range of fatal errors that PHP does not directly provide tools for you to handle them.[...]…

  3. Martin Wood says:

    Thanks, Michael.

    That was exactly what I was looking for to deal with a problem I have dealing with potentially invalid PHP files that are generated from user input.

  4. rob says:

    I would not have thought that register_shutdown_function could catch E_PARSE, when even set_error_handler cannot. This is great, thanks!

  5. Claude says:

    Please change the title of your article to something more search engine friendly. Something like this would be much better:

    PhP Fatal Error Handling

  6. Christian S. says:

    Hey, I am happy about this blog entry. I searched for a solution to catch fatal errors and with Your help I could solve my problem :-)
    Thank You very much!

  7. andi zaugg says:

    cool!
    thank you very much!

  8. beakt says:

    This is a terrific article. Not the usual regurgitating of the PHP manual that you usually find when you Google for new knowledge about PHP. This article is unique and clever. Very useful, Michael.

    I implemented this on my system tonight. Now, when I forget a semi-colon or screw up my brackets, I can have my web server (by checking $_SERVER['remote_addr']) display the error only for me, and display a friendly apology to my users. I don’t have to go to my Putty window and run ‘tail phperror.log’ any more!

    I can add this: As you may have noticed, this method won’t catch a parse error if the definition/registration of the shutdown function is in the same file. But, if you put the definition/registration of the shutdown function into an auto_prepend_file, then it will catch a parse error in your main PHP file called via the URI.

Leave a Reply

You must be logged in to post a comment.