Leones del desierto e introspección

Probablemente, casi todos los habitantes de Habr saben qué es una dicotomía y cómo usarla para atrapar un león en el desierto. Los errores en los programas también pueden detectarse con una dicotomía, especialmente en ausencia de información diagnóstica sensata.

imagen

Una vez que depuré mi proyecto en PHP / Laravel, vi este error en el navegador:

imagen

Esto fue, al menos, extraño, porque, a juzgar por la descripción en RFC 2616, un error 502 significa que "el Servidor, actuando como puerta de enlace o proxy, recibió una respuesta incorrecta del servidor ascendente". En mi caso, no había puertas de enlace, no había proxy entre el servidor web y el navegador, el servidor web nginx se ejecutaba en virtualbox y entregaba contenido web directamente, sin ningún intermediario. Los registros de nginx tenían esto:

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"

Las palabras "servidor ascendente" en la descripción del error 502 ("servidor ascendente" en la versión original en inglés del RFC) sugirieron algunos servidores de red adicionales en la ruta de solicitud desde el navegador a nginx, pero, aparentemente, en este caso, el mencionado en el mensaje El módulo PHP-FPM, al ser un programa de servidor, actúa como este servidor muy corriente arriba. En los registros de PHP, esto fue:

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

Ahora estaba claro dónde se produce el problema, pero su causa no estaba clara. PHP acaba de caer en el volcado del núcleo, sin mostrar ninguna información sobre en qué punto de la interpretación del programa PHP se produjo un error. Así que ha llegado el momento de atrapar un león en el desierto, para usar mi método favorito de depuración por dicotomía en tales casos. Anticipando objeciones en los comentarios, noto que uno podría usar un depurador aquí, por ejemplo, el mismo XDebug, pero la dicotomía fue más interesante. Además, el turno llegará a XDebug.

Entonces, en la forma de procesar la solicitud web, configuré la salida de diagnóstico más simple, con la finalización posterior del programa, para asegurarme de que no se produzca ningún error en el lugar de su instalación:

 echo “I am here”; die(); 

Ahora la página mala se veía así:

imagen

Después de poner el comando escrito anteriormente, primero al principio y luego al final de la ruta de procesamiento de la solicitud web, descubrí que se produce un error (¡quién lo dudaría!) En algún lugar entre estos dos puntos. Después de configurar los diagnósticos en el medio de la ruta de solicitud web, descubrí que el error aparece en algún lugar cerca del final. Después de un par de iteraciones de este tipo, me di cuenta de que el error no ocurre en el controlador de la arquitectura Laravel MVC en sí, sino que ya está en la salida, al representar la vista, que es la más simple aquí, en este espíritu:

 @extends('layouts.app') @section('content') <div> <div class="panel-heading">Myservice</div> <div class="panel-body"></div> </div> @endsection 
Como puede ver, la plantilla de vista no contiene código PHP (el motor de plantillas de Laravel le permite usar el código PHP a la vista), y los problemas ciertamente no están aquí. Pero arriba vemos que esta vista hereda la plantilla layouts.app, así que mire allí. Ya es más complicado: hay elementos de navegación, formularios de inicio de sesión y otras cosas comunes a todas las páginas del servicio. Omitiendo todo lo que está allí, solo daré una línea, debido a que surgió una falla, se encontró la misma dicotomía. Aquí está la línea:

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

Aquí, solo en el código de la plantilla de vista, se usó PHP. Era mi "encanto": la derivación de constantes de back-end, en forma de código JS, para usarlas en la interfaz, en nombre del principio DRY. El método loadBackendConstantsAsJSData enumera varias clases con las constantes necesarias en la interfaz. El error ocurrió en el método addClassConstants utilizado por él, donde se utilizó la introspección de PHP para obtener una lista de constantes de clase:

 /** * 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); } 

Después de buscar entre las clases con constantes pasadas a este método, resultó que la razón de todo, esta clase con constantes, es la ruta a los métodos de la API REST.

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

Hay bastantes líneas en él, y para encontrar la correcta, una dicotomía fue nuevamente útil. Ahora, espero que todos noten que self :: falta en la definición de la constante frente al nombre de la constante DATA_API. Después de agregarlo a su lugar legítimo, todo funcionó.

Habiendo decidido que el problema está en el mecanismo de introspección, comencé a escribir un ejemplo mínimo para reproducir un error:

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

Sin embargo, al ejecutar este script, PHP no iba a fallar, sino que emitió una advertencia completamente sensata.

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

En este punto, ya estaba convencido de que el problema se manifiesta no solo al cargar el sitio, sino también al ejecutar el código escrito anteriormente a través de la línea de comando. La única diferencia entre el tiempo de ejecución y el script mínimo fue la presencia del contexto de Laravel: el código del problema se ejecutó a través de su utilidad artesanal. Entonces bajo Laravel había algún tipo de diferencia. Para entender de qué se trata, es hora de usar el depurador. Al ejecutar el código bajo xdebug, vi que el bloqueo ocurre después de llamar al método ReflectionClass :: getConstants en el método Illuminate \ Foundation \ Bootstrap \ HandleExceptions :: handleError, que se ve muy simple:

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

El hilo de ejecución llegó allí después de lanzar una excepción debido al mismo error al describir la constante desde la que todo comenzó, y PHP se bloqueó al intentar lanzar una ErrorException. Una excepción en el controlador de excepciones ... Inmediatamente recordé la famosa Doble falla . Por lo tanto, para provocar un error, debe instalar controladores de excepciones similares a los de Laravel. Un poco más arriba en el código fue solo el método bootstrap que hizo esto:

Ahora, el ejemplo mínimo finalizado se veía así:

 <?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(); 

y su lanzamiento empaquetó constantemente el intérprete PHP versión 7.2.4 en el volcado del núcleo.

Parece que aquí hay una recursión interminable: cuando se maneja una excepción del error original, la siguiente excepción se lanza en handleException, se maneja nuevamente en handleException, y así hasta el infinito. Además, para reproducir el error, debe configurar error_handler y exception_handler, si solo uno de ellos está configurado, entonces el problema no se produce. También no pudo simplemente lanzar una excepción, en lugar de arrojar un error, parece que esto no es una recursión ordinaria, sino algo así como una dependencia circular.

Después de eso, busqué un problema en diferentes versiones de PHP (¡gracias, Docker!). Resultó que la falla solo se manifiesta, comenzando con la versión de PHP 7.1, las versiones anteriores de PHP funcionan correctamente: juran por la excepción ErrorException no detectada.

¿Qué conclusiones se pueden sacar de todo esto?

  1. Depuración por dicotomía, aunque es un método antediluviano de depuración, pero a veces puede ser necesario, especialmente en condiciones de falta de información de diagnóstico
  2. En mi opinión, los errores 502 son ininteligibles, tanto el mensaje al respecto ("Puerta de enlace incorrecta") como su decodificación en el RFC sobre la "respuesta incorrecta del servidor ascendente". Aunque, si considera los módulos conectados al servidor web como programas de servidor, puede comprender el significado de la decodificación de errores en RFC. Sin embargo, supongamos que el mismo PHP-FPM en la documentación se llama módulo y no servidor.
  3. Las unidades del analizador estático informarán inmediatamente de un error en la descripción de la constante. Pero entonces el error no sería atrapado.

Permítanme terminar con esto, ¡gracias a todos por su atención!

Bagreport - enviado .
UPD: el error está solucionado . Sin embargo, a juzgar por el código, terminó en el mecanismo de reflexión, en el manejo de errores del método ReflectionClass :: getConstants

Source: https://habr.com/ru/post/es414709/


All Articles