Provavelmente, quase todos os habitantes de Habr sabem o que é uma dicotomia e como usá-la para capturar um leão no deserto. Os erros nos programas também podem ser detectados com uma dicotomia, especialmente na ausência de informações de diagnóstico sensatas.

Depois de depurar meu projeto no PHP / Laravel, vi este erro no navegador:

Isso foi, pelo menos, estranho, porque, a julgar pela descrição na RFC 2616, um erro 502 significa que "o servidor, agindo como gateway ou proxy, recebeu uma resposta incorreta do servidor upstream". No meu caso, não havia gateways, não havia proxy entre o servidor da Web e o navegador, o servidor da Web estava executando o nginx na caixa virtual e fornecendo conteúdo da Web diretamente, sem intermediários. Os logs do nginx tinham o seguinte:
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"
As palavras “servidor upstream” na descrição do erro 502 (“servidor upstream” na versão original em inglês da RFC) sugeriram alguns servidores de rede adicionais no caminho da solicitação do navegador para o nginx, mas, aparentemente, nesse caso, o mencionado na mensagem o módulo PHP-FPM, sendo um programa de servidor, atua como esse servidor muito upstream. Nos logs do PHP, isso era:
[20-Jun-2018 13:42:41] WARNING: [pool www] child 26098 exited on signal 11 (SIGSEGV - core dumped) after 102247.908379 seconds from start
Agora estava claro onde o problema surge, mas sua causa não era clara. O PHP acabou de cair no core dump, não exibindo nenhuma informação sobre em que ponto da interpretação do programa PHP ocorreu um erro. Então chegou a hora de pegar um leão no deserto - para usar o meu método favorito de depuração por dicotomia nesses casos. Antecipando objeções nos comentários, observo que alguém poderia usar um depurador aqui, por exemplo, o mesmo XDebug, mas a dicotomia era mais interessante. Além disso, a vez chegará ao XDebug.
Portanto, na maneira de processar a solicitação da Web, defino a saída de diagnóstico mais simples, com a conclusão do programa, para garantir que nenhum erro ocorra no local de sua instalação:
echo “I am here”; die();
Agora a página ruim estava assim:

Depois de colocar o comando escrito acima, primeiro no início e depois no final do caminho de processamento da solicitação da Web, descobri que um erro (quem duvidaria disso!) Ocorre em algum lugar entre esses dois pontos. Depois de definir o diagnóstico no meio do caminho da solicitação da Web, descobri que o erro aparece em algum lugar próximo ao final. Após algumas dessas iterações, percebi que o erro não ocorre no controlador da arquitetura do Laravel MVC, mas já na saída dela, ao renderizar a visualização, que é a mais simples aqui, neste espírito:
@extends('layouts.app') @section('content') <div> <div class="panel-heading">Myservice</div> <div class="panel-body"></div> </div> @endsection
Como você pode ver, o modelo de visualização não contém código PHP (o mecanismo de modelos do Laravel permite que você use o código PHP na visualização), e os problemas certamente não estão aqui. Mas, acima, vemos que essa visualização herda o modelo layouts.app, então veja lá. Já é mais complicado: existem elementos de navegação, formulários de login e outras coisas comuns a todas as páginas do serviço. Omitindo tudo o que existe, darei apenas uma linha, devido à qual ocorreu uma falha, foi encontrada a mesma dicotomia. Aqui está a linha:
<script> window.bkConst = {!! (new App\Src\Helpers\UtilsHelper())->loadBackendConstantsAsJSData() !!}; </script>
Aqui, apenas no código do modelo de exibição, o PHP foi usado. Era o meu "encanto" - a derivação de constantes de back-end, na forma de código JS, por usá-las no front-end, em nome do princípio DRY. O método loadBackendConstantsAsJSData lista várias classes com as constantes necessárias no frontend. O erro ocorreu no método addClassConstants usado por ele, onde a introspecção do PHP foi usada para obter uma lista de constantes de classe:
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); }
Após pesquisar entre as classes com constantes passadas para esse método, descobriu-se que o motivo de tudo - essa classe com constantes - é o caminho para os métodos da 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"; }
Existem algumas linhas e, para encontrar a correta, uma dicotomia foi novamente útil. Agora, espero que todos tenham notado que self :: está ausente na definição da constante na frente do nome da constante DATA_API. Depois de adicioná-lo ao seu devido lugar, tudo funcionou.
Tendo decidido que o problema está no mecanismo de introspecção, comecei a escrever um exemplo mínimo para reproduzir um bug:
class SomeConstants { const SOME_CONSTANT = SOME_NONSENSE; } $r = new \ReflectionClass(SomeConstants::class); $r->getConstants();
No entanto, ao executar esse script, o PHP não falharia, mas emitiu um aviso completamente sensato.
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
Nesse ponto, eu já estava convencido de que o problema se manifestava não apenas ao carregar o site, mas também ao executar o código escrito acima através da linha de comando. A única diferença entre o tempo de execução e o script mínimo foi a presença do contexto do Laravel: o código do problema foi executado por meio de seu utilitário artesanal. Então, no Laravel, havia algum tipo de diferença. Para entender o que é, é hora de usar o depurador. Executando o código no xdebug, vi que a falha ocorre depois de chamar o método ReflectionClass :: getConstants no método Illuminate \ Foundation \ Bootstrap \ HandleExceptions :: handleError, que parece muito simples:
public function handleError($level, $message, $file = '', $line = 0, $context = []) { if (error_reporting() & $level) { throw new ErrorException($message, 0, $level, $file, $line); } }
O thread de execução chegou lá depois de lançar uma exceção devido ao próprio erro ao descrever a constante a partir da qual tudo começou, e o PHP travou ao tentar lançar uma ErrorException. Uma exceção no manipulador de exceções ... Lembrei-me imediatamente da famosa
falha dupla . Portanto, para causar uma falha, você precisa instalar manipuladores de exceção semelhantes aos do Laravel. Um pouco mais alto no código foi apenas o método de inicialização que fez isso:
Agora, o exemplo mínimo finalizado ficou assim:
<?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();
e seu lançamento empacotou constantemente o interpretador PHP versão 7.2.4 no core dump.
Parece que há uma recursão infinita aqui - ao manipular uma exceção do erro original, a próxima exceção é lançada em handleException, manipulada novamente em handleException e assim por diante até o infinito. Além disso, para reproduzir a falha, você precisa definir o error_handler e o exception_handler, se apenas um deles estiver definido, o problema não ocorrerá. Ele também falhou ao simplesmente lançar uma exceção, em vez de gerar um erro, parece que isso não é uma recursão bastante comum, mas algo como uma dependência circular.
Depois disso, verifiquei um problema em diferentes versões do PHP (obrigado, Docker!). Acontece que a falha apenas se manifesta, começando com a versão do PHP 7.1, versões anteriores do PHP funcionam corretamente - eles juram pela exceção ErrorException não detectada.
Que conclusões podem ser tiradas disso tudo?
- Depuração por dicotomia, embora seja um método antediluviano de depuração, mas às vezes pode ser necessário, especialmente em condições de falta de informações de diagnóstico
- Na minha opinião, os erros 502 são ininteligíveis, tanto a mensagem sobre ele (“gateway incorreto”) quanto sua decodificação no RFC sobre a “resposta incorreta do servidor upstream”. Embora, se você considerar os módulos conectados ao servidor Web como programas de servidor, poderá entender o significado da decodificação de erros no RFC. No entanto, digamos que o mesmo PHP-FPM na documentação seja chamado de módulo e não de servidor.
- Analisador estático, ele reportaria imediatamente um erro na descrição da constante. Mas o bug não seria pego.
Deixe-me terminar com isso, obrigado a todos pela atenção!
Bagreport -
enviado .
UPD: o bug foi
corrigido . A julgar pelo código, ele terminou no mecanismo de reflexão - no tratamento de erros do método ReflectionClass :: getConstants