PHP will (as of version 8) happily send a 200 response when there's a fatal error like, for example, a syntax error on an autoloaded class, even if you define an error handler using set_error_handler that will output a different HTTP status code.

The problem is, set_error_handler doesn't work on all error types. A workaround is adding a shutdown function using register_shutdown_function that checks the last error's type.

For example:

/**
 * This function catches some errors, but not all of them.
 */

set_error_handler(function ($errno, $errstr, $errfile, $errline) {
    header('HTTP/1.1 500 Internal Server Error', TRUE, 500);
});

/**
 * This one is needed to catch fatal errors and set the HTTP status code appropriately.
 */

register_shutdown_function(function () {
    $last_error = error_get_last();
    if ($last_error && in_array($last_error['type'], [E_ERROR, E_PARSE, E_COMPILE_ERROR, E_CORE_ERROR]))
        header('HTTP/1.1 500 Terrible Internal Server Error', TRUE, 500);
});

/**
 * Without this, in the "terrible" case we'll have already sent output so we won't be able to set the header. The output
 * sent is the "Fatal error" message. It's left as an exercise for the reader to figure out how to hide that message.
 */

ob_start();

if (isset($_GET['bad']))
    $a = new BadClass(); // Constructor triggers a user error

if (isset($_GET['terrible']))
    $a = new TerribleClass(); // Constructor triggers a fatal error
Final thoughts

You'd think PHP, a language that primarily deals with web stuff, would do this automatically! 200 responses on fatal errors can lead to catastrophic problems that you won't notice until it's way too late because all the server logs will think things have been going fine for, in my case, upwards of a month.

Previous on PHP