Wahrscheinlich wissen fast alle Einwohner von Habr, was eine Zweiteilung ist und wie man damit einen Löwen in der Wüste fängt. Fehler in Programmen können auch mit einer Zweiteilung aufgefangen werden, insbesondere wenn keine vernünftigen Diagnoseinformationen vorliegen.

Beim Debuggen meines Projekts in PHP / Laravel wurde dieser Fehler im Browser angezeigt:

Dies war zumindest seltsam, da nach der Beschreibung in RFC 2616 ein 502-Fehler bedeutet, dass „der Server, der als Gateway oder Proxy fungiert, eine falsche Antwort vom Upstream-Server erhalten hat“. In meinem Fall gab es keine Gateways, es gab keinen Proxy zwischen dem Webserver und dem Browser, der Webserver lief nginx unter virtualbox und lieferte Webinhalte direkt ohne Zwischenhändler. Die Nginx-Protokolle hatten Folgendes:
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"
Die Wörter "Upstream-Server" in der Beschreibung des 502-Fehlers ("Upstream-Server" in der englischen Originalversion des RFC) deuteten auf einige zusätzliche Netzwerkserver auf dem Anforderungspfad vom Browser zu nginx hin, aber anscheinend in diesem Fall auf den in der Nachricht genannten Das PHP-FPM-Modul ist ein Serverprogramm und fungiert als dieser Upstream-Server. In den PHP-Protokollen war dies:
[20-Jun-2018 13:42:41] WARNING: [pool www] child 26098 exited on signal 11 (SIGSEGV - core dumped) after 102247.908379 seconds from start
Jetzt war klar, wo das Problem auftritt, aber die Ursache war unklar. PHP ist gerade in den Core Dump gefallen und zeigt keine Informationen darüber an, zu welchem Zeitpunkt bei der Interpretation des PHP-Programms ein Fehler aufgetreten ist. Es ist also an der Zeit, einen Löwen in der Wüste zu fangen - in solchen Fällen meine bevorzugte Methode zum Debuggen durch Dichotomie anzuwenden. Ich nehme Einwände in den Kommentaren vorweg und stelle fest, dass man hier einen Debugger verwenden könnte, zum Beispiel denselben XDebug, aber die Dichotomie war interessanter. Außerdem wird XDebug an der Reihe sein.
Bei der Verarbeitung der Webanforderung habe ich die einfachste Diagnoseausgabe mit dem weiteren Abschluss des Programms festgelegt, um sicherzustellen, dass am Ort der Installation kein Fehler auftritt:
echo “I am here”; die();
Jetzt sah die schlechte Seite so aus:

Nachdem ich den oben geschriebenen Befehl zuerst am Anfang und dann am Ende des Verarbeitungspfads für Webanfragen eingegeben hatte, stellte ich fest, dass irgendwo zwischen diesen beiden Punkten ein Fehler auftritt (wer würde das bezweifeln!). Nachdem ich die Diagnose in der Mitte des Webanforderungspfads festgelegt hatte, stellte ich fest, dass der Fehler gegen Ende auftritt. Nach einigen solchen Iterationen wurde mir klar, dass der Fehler nicht im Controller der Laravel MVC-Architektur selbst auftritt, sondern bereits am Ausgang, wenn die hier in diesem Sinne einfachste Ansicht gerendert wird:
@extends('layouts.app') @section('content') <div> <div class="panel-heading">Myservice</div> <div class="panel-body"></div> </div> @endsection
Wie Sie sehen können, enthält die Ansichtsvorlage keinen PHP-Code (mit der Laravel-Vorlagen-Engine können Sie PHP-Code in der Ansicht verwenden), und die Probleme sind sicherlich nicht hier. Oben sehen wir jedoch, dass diese Ansicht die Vorlage layouts.app erbt. Schauen Sie also dort nach. Es ist bereits komplizierter: Es gibt Navigationselemente, Anmeldeformulare und andere Dinge, die allen Seiten des Dienstes gemeinsam sind. Wenn ich alles weglasse, was da ist, werde ich nur eine Zeile geben, aufgrund derer ein Fehler aufgetreten ist. Es wurde die gleiche Zweiteilung festgestellt. Hier ist die Zeile:
<script> window.bkConst = {!! (new App\Src\Helpers\UtilsHelper())->loadBackendConstantsAsJSData() !!}; </script>
Hier wurde nur im Code der Ansichtsvorlage PHP verwendet. Es war mein „Charme“ - die Ableitung von Backend-Konstanten in Form von JS-Code, um sie im Namen des DRY-Prinzips im Frontend zu verwenden. Die Methode loadBackendConstantsAsJSData listet mehrere Klassen mit den erforderlichen Konstanten im Frontend auf. Der Fehler trat in der von ihm verwendeten addClassConstants-Methode auf, bei der die PHP-Introspektion verwendet wurde, um eine Liste der Klassenkonstanten abzurufen:
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); }
Nach der Suche in den Klassen mit Konstanten, die an diese Methode übergeben wurden, stellte sich heraus, dass der Grund für alles - diese Klasse mit Konstanten - der Pfad zu den REST-API-Methoden ist.
class APIPath { const API_BASE_PATH = '/api/v1'; const DATA_API = self::API_BASE_PATH . "/data"; ... const DATA_ADDITIONAL_API = DATA_API . "/additional"; }
Es gibt einige Zeilen darin, und um die richtige zu finden, war eine Dichotomie wieder nützlich. Nun hoffe ich, dass jeder bemerkt hat, dass self :: in der Definition der Konstante vor dem Konstantennamen DATA_API fehlt. Nachdem es an seinen rechtmäßigen Platz hinzugefügt wurde, funktionierte alles.
Nachdem ich festgestellt hatte, dass das Problem im Introspektionsmechanismus liegt, begann ich, ein minimales Beispiel für die Reproduktion eines Fehlers zu schreiben:
class SomeConstants { const SOME_CONSTANT = SOME_NONSENSE; } $r = new \ReflectionClass(SomeConstants::class); $r->getConstants();
Beim Ausführen dieses Skripts stürzte PHP jedoch nicht ab, sondern gab eine völlig vernünftige Warnung aus.
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
Zu diesem Zeitpunkt war ich bereits davon überzeugt, dass sich das Problem nicht nur beim Laden der Site manifestiert, sondern auch beim Ausführen des oben über die Befehlszeile geschriebenen Codes. Der einzige Unterschied zwischen der Laufzeit und dem Minimal-Skript war das Vorhandensein des Laravel-Kontexts: Der Problemcode wurde über das handwerkliche Dienstprogramm ausgeführt. Unter Laravel gab es also einen Unterschied. Um zu verstehen, was es ist, ist es Zeit, den Debugger zu verwenden. Beim Ausführen des Codes unter xdebug stellte ich fest, dass der Absturz bereits nach dem Aufrufen der ReflectionClass :: getConstants-Methode in der Illuminate \ Foundation \ Bootstrap \ HandleExceptions :: handleError-Methode auftritt, die sehr einfach aussieht:
public function handleError($level, $message, $file = '', $line = 0, $context = []) { if (error_reporting() & $level) { throw new ErrorException($message, 0, $level, $file, $line); } }
Der Ausführungsthread kam nach dem Auslösen einer Ausnahme aufgrund des Fehlers bei der Beschreibung der Konstante, von der aus alles gestartet wurde, und PHP stürzte ab, als versucht wurde, eine ErrorException auszulösen. Eine Ausnahme im Ausnahmebehandler ... Ich erinnerte mich sofort an den berühmten
Doppelfehler . Um einen Fehler zu verursachen, müssen Sie Ausnahmebehandlungsroutinen installieren, die denen von Laravel ähneln. Etwas höher im Code war nur die Bootstrap-Methode, die dies tat:
Das endgültige Minimalbeispiel sah nun folgendermaßen aus:
<?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();
und sein Start packte den PHP Version 7.2.4 Interpreter stetig in Core Dump.
Es scheint, dass es hier eine endlose Rekursion gibt - wenn eine Ausnahme vom ursprünglichen Fehler behandelt wird, wird die nächste Ausnahme in handleException ausgelöst, erneut in handleException behandelt und so weiter bis ins Unendliche. Um den Fehler zu reproduzieren, müssen Sie außerdem sowohl error_handler als auch exception_handler festlegen. Wenn nur einer von ihnen festgelegt ist, tritt das Problem nicht auf. Es ist auch nicht gelungen, einfach eine Ausnahme auszulösen, anstatt einen Fehler auszulösen. Es scheint, dass dies keine ganz normale Rekursion ist, sondern so etwas wie eine zirkuläre Abhängigkeit.
Danach habe ich unter verschiedenen PHP-Versionen nach einem Problem gesucht (danke, Docker!). Es stellte sich heraus, dass sich der Fehler erst ab der Version von PHP 7.1 manifestiert. Frühere Versionen von PHP funktionieren ordnungsgemäß - sie schwören auf die nicht erfasste ErrorException-Ausnahme.
Welche Schlussfolgerungen können daraus gezogen werden?
- Debuggen durch Dichotomie, obwohl es sich um eine antidiluvianische Debugging-Methode handelt, kann es jedoch manchmal erforderlich sein, insbesondere bei fehlenden diagnostischen Informationen
- Meiner Meinung nach sind die 502-Fehler unverständlich, sowohl die Meldung darüber ("Bad Gateway") als auch die Dekodierung im RFC über die "falsche Antwort vom Upstream-Server". Wenn Sie die mit dem Webserver verbundenen Module als Serverprogramme betrachten, können Sie die Bedeutung der Fehlerdecodierung in RFC verstehen. Nehmen wir jedoch an, dass dasselbe PHP-FPM in der Dokumentation als Modul und nicht als Server bezeichnet wird.
- Bei statischen Analysatorantrieben wird sofort ein Fehler in der Beschreibung der Konstante gemeldet. Aber dann würde der Fehler nicht gefangen werden.
Lassen Sie mich damit fertig werden, ich danke Ihnen allen für Ihre Aufmerksamkeit!
Bagreport -
gesendet .
UPD: Der Fehler ist
behoben . Dem Code nach zu urteilen, landete es dennoch im Reflexionsmechanismus - in der Fehlerbehandlung der ReflectionClass :: getConstants-Methode