沙漠狮子与自省

哈勃(Habr)的几乎所有居民大概都知道什么是二分法,以及如何用它在沙漠中捉狮子。 二分法也可以捕获程序中的错误,尤其是在没有健全的诊断信息的情况下。

图片

在PHP / Laravel中调试项目后,我在浏览器中看到此错误:

图片

至少这很奇怪,因为根据RFC 2616中的描述判断,出现502错误意味着“充当网关或代理的服务器从上游服务器接收到错误的响应。” 以我为例,没有网关,Web服务器和浏览器之间没有代理,Web服务器是在virtualbox下运行的nginx,无需任何中介就可以直接传递Web内容。 Nginx日志具有以下内容:

2018/06/20 13:42:41 [error] 2791#2791: *2206 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 192.168.10.1, server: colg.test, request: "GET / HTTP/1.1", upstream: "fastcgi://unix:/var/run/php/php7.1-fpm.sock:", host: "colg.test"

502错误描述中的“上游服务器”一词(RFC的原始英文版中为“上游服务器”)在从浏览器到nginx的请求路径上建议了一些其他网络服务器,但显然,在这种情况下,消息中提到的服务器作为服务器程序的PHP-FPM模块充当此上游服务器。 在PHP日志中,这是:

[20-Jun-2018 13:42:41] WARNING: [pool www] child 26098 exited on signal 11 (SIGSEGV - core dumped) after 102247.908379 seconds from start

现在很清楚问题出在哪里,但原因尚不清楚。 PHP只是放入了核心转储中,没有显示任何有关在解释PHP程序时发生错误的信息。 因此,到了在沙漠中捉住狮子的时候了-在这种情况下,使用我最喜欢的二分法调试方法。 预计评论中会出现异议,我注意到这里可以使用调试器,例如相同的XDebug,但是二分法更有趣。 此外,该轮到XDebug了。

因此,在处理Web请求的方式中,我设置了最简单的诊断输出,并进一步完成了该程序,以确保其安装位置不会发生错误:

 echo “I am here”; die(); 

现在,坏页面看起来像这样:

图片

将上面编写的命令放在Web请求处理路径的开头和结尾之后,我发现在这两点之间的某个地方出现了一个错误(谁会怀疑!)。 在Web请求路径的中间设置了诊断程序后,我发现该错误出现在末尾附近。 经过几次这样的迭代,我意识到该错误不会在Laravel MVC体系结构本身的控制器中发生,而是在呈现视图时已经在它的出口处发生了,这是本着这种精神最简单的方法:

 @extends('layouts.app') @section('content') <div> <div class="panel-heading">Myservice</div> <div class="panel-body"></div> </div> @endsection 
如您所见,视图模板不包含PHP代码(Laravel模板引擎允许您在视图中使用PHP代码),而且问题肯定不在这里。 但在上方我们看到该视图继承了layouts.app模板,因此请看那里。 它已经更加复杂:服务的所有页面都有导航元素,登录表单和其他公用内容。 省略那里的所有内容,我只给一行,由于出现了故障,人们发现它都是相同的二分法。 这是一行:

 <script> window.bkConst = {!! (new App\Src\Helpers\UtilsHelper())->loadBackendConstantsAsJSData() !!}; </script> 

在这里,仅在视图模板的代码中就使用了PHP。 这是我的“魅力”-以DRY原理的名义,以JS代码的形式导出后端常量,以便在前端使用它们。 loadBackendConstantsAsJSData方法列出了几个类,这些类在前端具有必需的常量。 错误发生在他使用的addClassConstants方法中,其中使用了PHP自省以获取类常量的列表:

 /** * add all class constants to resulted JSON * @param string $classFullName */ private function addClassConstants(string $classFullName, array &$constantsArray) { $r = new ReflectionClass($classFullName); $result = []; $className = $r->getShortName(); $classConstants = $r->getConstants(); foreach($classConstants as $name => $value) { if (is_array($value) || is_object($value)) { continue; } $result["$className::$name"] = $value; } $constantsArray = array_merge($constantsArray, $result); } 

在使用传递给此方法的常量的类中进行搜索之后,事实证明,所有原因(带有常量的此类)的原因都是REST API方法的路径。

 class APIPath { const API_BASE_PATH = '/api/v1'; const DATA_API = self::API_BASE_PATH . "/data"; ... const DATA_ADDITIONAL_API = DATA_API . "/additional"; } 

其中有很多行,为了找到正确的行,二分法再次有用。 现在,我希望每个人都注意到常量名DATA_API前面的常量定义中缺少self ::。 将其添加到正确的位置后,一切正常。

确定问题出在自省机制之后,我开始写一个最小的示例来再现错误:

 class SomeConstants { const SOME_CONSTANT = SOME_NONSENSE; } $r = new \ReflectionClass(SomeConstants::class); $r->getConstants(); 

但是,运行此脚本时,PHP不会崩溃,但会发出完全理智的警告。

PHP Warning: Use of undefined constant SOME_NONSENSE - assumed 'SOME_NONSENSE' (this will throw an Error in a future version of PHP) in /home/vagrant/code/colg/_tmp/1.php on line 17

至此,我已经确信该问题不仅在加载站点时表现出来,而且在执行通过命令行编写的上述代码时也表现出来。 运行时和最小脚本之间的唯一区别是Laravel上下文的存在:问题代码通过其artisan实用程序运行。 因此,在Laravel领导下存在某种差异。 要了解它是什么,是时候使用调试器了。 在xdebug下运行代码,我发现在Illuminate \ Foundation \ Bootstrap \ HandleExceptions :: handleError方法中调用ReflectionClass :: getConstants方法之后,崩溃已经发生,该方法非常简单:

 public function handleError($level, $message, $file = '', $line = 0, $context = []) { if (error_reporting() & $level) { throw new ErrorException($message, 0, $level, $file, $line); } } 

执行线程在抛出异常后到达那里,这是因为描述它的所有常量的错误非常严重,并且在尝试抛出ErrorException时PHP崩溃了。 异常处理程序中的异常...我立刻想起了著名的Double错误 。 因此,要导致失败,您需要安装类似于Laravel的异常处理程序。 代码中稍高一点的是执行此操作的bootstrap方法:

现在,最终的最小示例如下所示:

 <?php class SomeConstants { const SOME_CONSTANT = SOME_NONSENSE; } function handleError() { throw new ErrorException(); } set_error_handler('handleError'); set_exception_handler('handleError'); $r = new \ReflectionClass(SomeConstants::class); $r->getConstants(); 

并且其启动将PHP版本7.2.4解释器稳定地打包到了核心转储中。

似乎这里有无限递归-处理来自原始错误的异常时,下一个异常将在handleException中引发,在handleException中再次处理,依此类推直至无限。 此外,要重现故障,您需要同时设置error_handler和exception_handler,如果只设置了其中之一,则不会发生此问题。 它也不能简单地引发异常,而不是引发错误,这似乎不是很普通的递归,而是类似循环依赖的东西。

之后,我检查了不同版本的PHP下的问题(感谢Docker!)。 事实证明,只有从PHP 7.1版本开始,故障才开始显现,而早期版本的PHP可以正常工作-他们宣誓会遇到未捕获的ErrorException异常。

从这一切可以得出什么结论?

  1. 通过二分法进行调试,虽然它是以前的调试方法,但是有时可能是必要的,尤其是在缺少诊断信息的情况下
  2. 我认为,502错误是无法理解的,无论是关于它的消息(“错误网关”),还是在RFC中关于“来自上游服务器的错误响应”的解码。 尽管,如果将连接到Web服务器的模块视为服务器程序,则可以理解RFC中错误解码的含义。 但是,假设文档中相同的PHP-FPM被称为模块而不是服务器。
  3. 静态分析器驱动器,它将立即报告该常量的描述错误。 但是,此错误将不会被捕获。

让我结束这个,谢谢大家的关注!

Bagreport已发送
UPD:该错误已修复 。 从代码来看,它仍然以反射机制结尾-在ReflectionClass :: getConstants方法的错误处理中

Source: https://habr.com/ru/post/zh-CN414709/


All Articles