PHP与500错误

PHP开发过程中经常会遇到返回500错误的情况,而且body体中也没有任何调试(可用)内容。这个时候你就需要慢慢调试了(打断点,开调试模式等),但如果是现网,这个错误就比较让人抓狂了,既不好打断点也不能开调试模式。但既然是错误,总是会有处理方法,下面就一步步分析500的成因及处理方案。

0x01、500错误

500错误,也叫Internal Server Error(内部服务错误),表示服务因未知错误导致无法处理请求。在PHP站点中一般是由PHP返回,也就是说,500错误一般都是PHP脚本的错误。

php-fpm抓包500

从上图中可以看出(Nginx+PHP-FPM架构),在PHP调用一个不存在的类时,脚本发生错误并返回500给Nginx(并且将错误信息也做了返回,只不过是卸载STDERR中)。

0x02、哪些错误异常会导致500

那么哪类错误会导致500错误呢,PHP所有的错误级别可以在PHP的官方文文档(http://php.net/manual/zh/errorfunc.constants.php)中查询到,而这其中错误级别为E_ERRORE_PARSEE_RECOVERABLE_ERRORE_USER_ERROR以及未捕获的异常等都会导致500错误。

E_ERROR级别错误导致的500

0x03、什么情况下错误不会返回500

上面说了,这个是PHP脚本的错误导致的,但是PHP脚本有了错误或异常一定会导致500吗?显然不是,即使在脚本有致命错误的情况下,依旧可以返回200。

display_errors配置选项

在基于python、nodejs等的web应用中,默认情况下,如果出现异常信息会被打印到控制台(STDERR/STDOUT)中。而在基于PHP-FPM架构的PHP中没有控制台可以打印,它的stderr和stdout被置为FastCGI中对应的STRDERR和STDOUT。如果将错误重定向到STDOUT中,错误会直接输出到响应中,并且状态码也会置为200。这个也是display_errors选项所实现的能力。

display_errors选项的配置需要通过ini_set来实现,PHP文档中关于display_errors的配置表明该值为字符串类型,实际使用中数字和布尔类型也可以打开或关闭该配置。

打开值

关闭值

'On'、1、'1'、true

'Off'、0、'0'、false

error_reporting配置

display_errors控制了PHP脚本发生错误时是否显示错误详情以及是否返回错误状态码,而error_reporting项则用来控制哪级别的错误可以被直接打印出来。

error_reporting的设置项可以通过error_reporting(E_ALL)ini_set('error_reporting', E_ALL)来配置,函数参数的详情可以参考PHP文档

需要注意的是,PHP本身是有错误日志的(error_loglog_errors两个配置项目),若发生错误,PHP会将改错误写入错误日志中,而哪些错误需要被写入是受error_reporting项的控制的。

在错误级别不匹配的情况下不显示错误详情

0x04、现网如何合理处理500

500错误发生已经说明PHP脚本无法正常运行了,这时候能做的只是捕获异常并记录异常到日志,以方便日后的调试和现网bug的处理。

PHP自带错误日志

PHP本身已经带了错误日志的记录,可以在php.ini中将log_errors项设置为On,并配合error_log配置项来指定错误日志的存放路径。

错误日志记录开关
日志路径设置

该错误日志的的写入不受display_errors的配置的控制。也就是说不管display_errors是否开启,错误都会记录到日志中。但是却受error_reporting配置的控制,如果当前错误级别跟error_reporting中的错误级别不匹配的话,错误不会写入日志中。即如果错误级别是E_ERROR,但是设置却为error_reporting(E_NOTICE),那么日志中不会出现E_ERROR的出错信息。

PHP错误日志记录各种类型的错误
错误级别不匹配导致的日志不写入

捕获错误异常记录

PHP提供了set_error_handlerregister_shutdown_functionset_exception_handlererror_get_last等相关的错误处理函数。可以通过函数将捕获到的错误信息写入指定日志来实现错误的记录。

函数的使用详情可以参考http://km.oa.com/group/19368/articles/show/302491,这里提供一个模版:

$previousHandler = set_exception_handler(function(Exception $ex) use (&$previousHandler) {
    call_user_func('exceptionHandler', $ex, $previousHandler);
});
set_error_handler('errorHandler');
register_shutdown_function('fatalErrorHandler');

function exceptionHandler(Exception $ex, $previousHandler)
{
    $info = array(
        $ex->getFile(),
        $ex->getLine(),
        $ex->getCode(),
        $ex->getMessage()
    );
    // 记录日志
    logPHPError($info);
    if (isset($previousHandler) && is_callable($previousHandler)) {
        call_user_func($previousHandler, $ex);
    }
}

/**
 * 框架错误处理函数
 * @param $errno
 * @param $errstr
 * @param $errfile
 * @param $errline
 * @return bool
 */
function errorHandler($errno = 0, $errstr = '', $errfile = '', $errline = 0)
{
    switch ($errno) {
        case E_WARNING:
            $errname = 'E_WARNING';
            break;
        case E_NOTICE:
            $errname = 'E_NOTICE';
            break;
        case E_STRICT:
            $errname = 'E_STRICT';
            break;
        case E_RECOVERABLE_ERROR:
            $errname = 'E_RECOVERABLE_ERROR';
            break;
        case E_DEPRECATED:
            $errname = 'E_DEPRECATED';
            break;
        case E_USER_ERROR:
            $errname = 'E_USER_ERROR';
            break;
        case E_USER_WARNING:
            $errname = 'E_USER_WARNING';
            break;
        case E_USER_NOTICE:
            $errname = 'E_USER_NOTICE';
            break;
        case E_USER_DEPRECATED:
            $errname = 'E_USER_DEPRECATED';
            break;
        default:
            restore_error_handler();
            return false;
    }
    // 记录日志
    $info = array(
        $errfile,
        $errline,
        $errname,
        $errstr
    );
    logPHPError($info);
    restore_error_handler();
    return false;
}

/**
 * Fatal error错误处理
 */
function fatalErrorHandler()
{
    if (($e = error_get_last()) && $e['type'] === E_ERROR) {
        $info = array(
            $e['file'],
            $e['line'],
            'E_ERROR',
            $e['message']
        );
        // 记录日志
        logPHPError($info);
    }
}

0x05 总结

总结起来,error_reporting是用于控制向浏览器或PHP错误日志输出错误信息级别的函数或配置,而display_errors则是控制是否向浏览器输出错误和告警信息。

由于PHP的错误日志是全局的,而且受到error_reporting的控制,因此推荐在业务中实现自己的错误(异常)捕获记录逻辑。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JAVA技术zhai

微服务:Java EE的拯救者还是掘墓人?

有人说,Java确实过于臃肿,经常“小题大做”。但PHP、Node.js扩展方面短板太明显,做小应用可以,大型应用就玩不转了。 另外,Java EE领域有太多优...

14170
来自专栏信数据得永生

米斯特白帽培训讲义(v2)漏洞篇 XSS

跨站脚本攻击(Cross Site Scripting),为不和层叠样式表(Cascading Style Sheets,CSS)的缩写混淆,故将跨站脚本攻击缩...

14060
来自专栏信数据得永生

米斯特白帽培训讲义(v2)实战篇 余闻同学录

http://download.csdn.net/download/u012513463/9701150

10330
来自专栏信数据得永生

米斯特白帽培训讲义 漏洞篇 越权

越权漏洞是 Web 应用程序中一种常见的安全漏洞。它的威胁在于一个账户即可控制全站用户数据。当然这些数据仅限于存在漏洞功能对应的数据。越权漏洞的成因主要是开发人...

8540
来自专栏信数据得永生

米斯特白帽培训讲义 工具篇 BruteXSS

BruteXSS 是一个非常强大和快速的跨站点脚本检测工具,可用于暴力注入参数。BruteXSS 从指定的词库加载多种有效载荷进行注入,并且使用指定的载荷和扫描...

15050
来自专栏信数据得永生

米斯特白帽培训讲义 漏洞篇 XSS

我们看到,这段代码中首先包含一个表单,用于向页面自己发送 GET 请求,带一个名为xss的参数。 然后 PHP 会读取该参数,如果不为空,则直接打印出来,我们看...

14630
来自专栏JAVA技术zhai

微服务:Java EE的拯救者还是掘墓人?

有人认为,微服务的大行其道是在给Java EE下达死刑判决书。也有人认为,Java EE已死的论调可笑至极。读者朋友,你们怎么看?

11430
来自专栏字根中文校对软件

Web新标准:Ajax 的一些资源汇总

     最近阅读技术新闻的时候Ajax这个名词不时地跳出来,说实在的我已经在我的english blog和这里列举了很多Ajax的资源了。无奈还要给大家分...

9830
来自专栏信数据得永生

米斯特白帽培训讲义(v2)漏洞篇 文件包含

文件包含就是将一个文件包含到自己的文件中执行。它可分为本地包含和远程包含,本地包含即包含本地磁盘上的文件,文件名称是相对路径或绝对路径,远程包含即包含互联网上的...

9330
来自专栏信数据得永生

米斯特白帽培训讲义(v2)信息收集

一般来说,使用了 CDN 的网站在不同地点的ping结果是不一样的。不过这里它直接写出了百度云加速节点。

17640

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励