Tenho trabalhos executados em vários trabalhadores de fila, que contêm algumas solicitações HTTP usando o Guzzle. No entanto, o bloco try-catch dentro deste trabalho não parece ser ativado GuzzleHttp\Exception\RequestException
quando estou executando esses trabalhos no processo em segundo plano. O processo em execução é um php artisan queue:work
trabalhador do sistema de filas do Laravel que monitora a fila e seleciona os trabalhos.
Em vez disso, a exceção lançada é uma das GuzzleHttp\Promise\RejectionException
com a mensagem:
A promessa foi rejeitada pelo motivo: erro cURL 28: A operação atingiu o tempo limite após 30001 milissegundos com 0 bytes recebidos (consulte https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Na verdade, isso é um disfarçado GuzzleHttp\Exception\ConnectException
(consulte https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 ), porque se eu executar um trabalho semelhante em um processo PHP regular que é acionado visitando um URL, recebo o ConnectException
pretendido com a mensagem:
erro cURL 28: A operação atingiu o tempo limite após 100 milissegundos com 0 de 0 bytes recebidos (consulte https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Código de exemplo que acionaria esse tempo limite:
try {
$c = new \GuzzleHttp\Client([
'timeout' => 0.1
]);
$response = (string) $c->get('https://example.com')->getBody();
} catch(GuzzleHttp\Exception\RequestException $e) {
// This occasionally gets catched when a ConnectException (child) is thrown,
// but it doesnt happen with RejectionException because it is not a child
// of RequestException.
}
O código acima gera um RejectionException
ou ConnectException
quando executado no processo de trabalho, mas sempre um ConnectException
quando testado manualmente através do navegador (pelo que sei).
Então, basicamente, o que deduzo é que isso RejectionException
está envolvendo a mensagem doConnectException
, no entanto, não estou usando os recursos assíncronos do Guzzle. Meus pedidos são simplesmente feitos em série. A única coisa que difere é que vários processos PHP podem estar fazendo chamadas HTTP do Guzzle ou que os trabalhos em si estão atingindo o tempo limite (o que deve resultar em uma exceção diferente da do Laravel Illuminate\Queue\MaxAttemptsExceededException
), mas não vejo como isso faz com que o código se comporte de maneira diferente.
Não consegui encontrar nenhum código dentro dos pacotes Guzzle que esteja usando php_sapi_name()
/PHP_SAPI
(que determina a interface usada) para executar coisas diferentes ao executar a partir da CLI em vez de um gatilho do navegador.
tl; dr
Por que o Guzzle me joga RejectionException
nos processos de trabalho, mas ConnectException
nos scripts PHP regulares acionados pelo navegador?
Editar 1
Infelizmente, não posso criar um exemplo mínimo reproduzível. Vejo muitas mensagens de erro no rastreador de problemas do Sentry, com a exceção exata mostrada acima. A fonte é declarada como Starting Artisan command: horizon:work
(que é o Laravel Horizon, supervisiona as filas do Laravel). Eu verifiquei novamente para ver se há uma discrepância entre as versões do PHP, mas os processos do site e do trabalhador executam o mesmo PHP 7.3.14
que está correto:
PHP 7.3.14-1+ubuntu18.04.1+deb.sury.org+1 (cli) (built: Jan 23 2020 13:59:16) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.14, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.3.14-1+ubuntu18.04.1+deb.sury.org+1, Copyright (c) 1999-2018, by Zend Technologies
- A versão cURL é
cURL 7.58.0
. - A versão Guzzle é
guzzlehttp/guzzle 6.5.2
- A versão do Laravel é
laravel/framework 6.12.0
Editar 2 (rastreamento de pilha)
GuzzleHttp\Promise\RejectionException: The promise was rejected with reason: cURL error 28: Operation timed out after 30000 milliseconds with 0 bytes received (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)
#44 /vendor/guzzlehttp/promises/src/functions.php(112): GuzzleHttp\Promise\exception_for
#43 /vendor/guzzlehttp/promises/src/Promise.php(75): GuzzleHttp\Promise\Promise::wait
#42 /vendor/guzzlehttp/guzzle/src/Client.php(183): GuzzleHttp\Client::request
#41 /app/Bumpers/Client.php(333): App\Bumpers\Client::callRequest
#40 /app/Bumpers/Client.php(291): App\Bumpers\Client::callFunction
#39 /app/Bumpers/Client.php(232): App\Bumpers\Client::bumpThread
#38 /app/Models/Bumper.php(206): App\Models\Bumper::post
#37 /app/Jobs/PostBumper.php(59): App\Jobs\PostBumper::handle
#36 [internal](0): call_user_func_array
#35 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(32): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}
#34 /vendor/laravel/framework/src/Illuminate/Container/Util.php(36): Illuminate\Container\Util::unwrapIfClosure
#33 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(90): Illuminate\Container\BoundMethod::callBoundMethod
#32 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(34): Illuminate\Container\BoundMethod::call
#31 /vendor/laravel/framework/src/Illuminate/Container/Container.php(590): Illuminate\Container\Container::call
#30 /vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(94): Illuminate\Bus\Dispatcher::Illuminate\Bus\{closure}
#29 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(130): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#28 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(105): Illuminate\Pipeline\Pipeline::then
#27 /vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(98): Illuminate\Bus\Dispatcher::dispatchNow
#26 /vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(83): Illuminate\Queue\CallQueuedHandler::Illuminate\Queue\{closure}
#25 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(130): Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}
#24 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(105): Illuminate\Pipeline\Pipeline::then
#23 /vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(85): Illuminate\Queue\CallQueuedHandler::dispatchThroughMiddleware
#22 /vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(59): Illuminate\Queue\CallQueuedHandler::call
#21 /vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(88): Illuminate\Queue\Jobs\Job::fire
#20 /vendor/laravel/framework/src/Illuminate/Queue/Worker.php(354): Illuminate\Queue\Worker::process
#19 /vendor/laravel/framework/src/Illuminate/Queue/Worker.php(300): Illuminate\Queue\Worker::runJob
#18 /vendor/laravel/framework/src/Illuminate/Queue/Worker.php(134): Illuminate\Queue\Worker::daemon
#17 /vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(112): Illuminate\Queue\Console\WorkCommand::runWorker
#16 /vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(96): Illuminate\Queue\Console\WorkCommand::handle
#15 /vendor/laravel/horizon/src/Console/WorkCommand.php(46): Laravel\Horizon\Console\WorkCommand::handle
#14 [internal](0): call_user_func_array
#13 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(32): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}
#12 /vendor/laravel/framework/src/Illuminate/Container/Util.php(36): Illuminate\Container\Util::unwrapIfClosure
#11 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(90): Illuminate\Container\BoundMethod::callBoundMethod
#10 /vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(34): Illuminate\Container\BoundMethod::call
#9 /vendor/laravel/framework/src/Illuminate/Container/Container.php(590): Illuminate\Container\Container::call
#8 /vendor/laravel/framework/src/Illuminate/Console/Command.php(201): Illuminate\Console\Command::execute
#7 /vendor/symfony/console/Command/Command.php(255): Symfony\Component\Console\Command\Command::run
#6 /vendor/laravel/framework/src/Illuminate/Console/Command.php(188): Illuminate\Console\Command::run
#5 /vendor/symfony/console/Application.php(1012): Symfony\Component\Console\Application::doRunCommand
#4 /vendor/symfony/console/Application.php(272): Symfony\Component\Console\Application::doRun
#3 /vendor/symfony/console/Application.php(148): Symfony\Component\Console\Application::run
#2 /vendor/laravel/framework/src/Illuminate/Console/Application.php(93): Illuminate\Console\Application::run
#1 /vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(131): Illuminate\Foundation\Console\Kernel::handle
#0 /artisan(37): null
A Client::callRequest()
função contém simplesmente um Guzzle Client no qual eu chamo $client->request($request['method'], $request['url'], $request['options']);
(então não estou usando requestAsync()
). Eu acho que tem algo a ver com a execução de trabalhos em paralelo que causa esse problema.
Editar 3 (solução encontrada)
Considere o seguinte caso de teste que faz uma solicitação HTTP (que deve retornar uma resposta 200 regular):
try {
$c = new \GuzzleHttp\Client([
'base_uri' => 'https://example.com'
]);
$handler = $c->getConfig('handler');
$handler->push(\GuzzleHttp\Middleware::mapResponse(function(ResponseInterface $response) {
// Create a fake connection exception:
$e = new \GuzzleHttp\Exception\ConnectException('abc', new \GuzzleHttp\Psr7\Request('GET', 'https://example.com/2'));
// These 2 lines both cascade as `ConnectException`:
throw $e;
return \GuzzleHttp\Promise\rejection_for($e);
// This line cascades as a `RejectionException`:
return \GuzzleHttp\Promise\rejection_for($e->getMessage());
}));
$c->get('');
} catch(\Exception $e) {
var_dump($e);
}
Agora, o que eu fiz originalmente foi chamar, rejection_for($e->getMessage())
que cria o seu próprio com RejectionException
base na cadeia de mensagens. Ligar rejection_for($e)
foi a solução correta aqui. A única coisa que resta a responder é se essa rejection_for
função é igual a uma simples throw $e
.
HandlerStack
?Respostas:
Olá Gostaria de saber se você está tendo o erro 4xx ou o erro 5xx
Mas mesmo assim vou colocar algumas alternativas para soluções encontradas que se assemelham ao seu problema
alternativa 1
Gostaria de dar uma olhada nisso, tive esse problema com um novo servidor de produção retornando 400 respostas inesperadas em comparação com o ambiente de desenvolvimento e teste funcionando conforme o esperado; simplesmente instalar o apt install php7.0-curl o corrigiu.
Foi uma nova instalação do Ubuntu 16.04 LTS com php instalado via ppa: ondrej / php, durante a depuração notei que os cabeçalhos eram diferentes. Ambos estavam enviando um formulário de várias partes com dados agrupados, no entanto, sem o php7.0-curl, estavam enviando um cabeçalho Connection: close ao invés do Expect: 100-Continue; os dois pedidos tiveram Transfer-Encoding: chunked.
alternativa 2
Talvez você deva tentar isso
Guzzle precisa cactching se o código de resposta não for 200
alternativa 3
No meu caso, foi porque eu havia passado uma matriz vazia nas opções $ da solicitação ['json'] e não consegui reproduzir o 500 no servidor usando Postman ou cURL, mesmo ao passar o cabeçalho Content-Type: application / json request.
De qualquer forma, remover a chave json da matriz de opções da solicitação resolveu o problema.
Passei 30 minutos tentando descobrir o que havia de errado, porque esse comportamento é muito inconsistente. Para todas as outras solicitações que estou fazendo, passar $ options ['json'] = [] não causou nenhum problema. Pode ser um problema do servidor, não controlo o servidor.
envie feedback sobre os detalhes obtidos
fonte
ConnectException
não tem uma resposta associada; portanto, não há erros de 400 ou 500 até onde sei. Parece que você realmente deveria estar pegandoBadResponseException
(ouClientException
(4xx) /ServerException
(5xx), que são ambos filhos dele)O Guzzle usa o Promises para solicitações síncronas e assíncronas. A única diferença é que, quando você usa uma solicitação síncrona (seu caso) - ela é atendida imediatamente chamando um
wait()
método . Observe esta parte:Portanto, ele lança
RequestException
qual é uma instância\Exception
e sempre ocorre nos erros HTTP 4xx e 5xx, a menos que o lançamento de exceções seja desativado por meio de opções. Como você vê, também pode gerar aRejectionException
se o motivo não for uma instância de,\Exception
por exemplo, se o motivo for uma string que parece acontecer no seu caso. O estranho é que você recebe, emRejectException
vez deRequestException
Guzzle, lançar umConnectException
erro de tempo limite de conexão. De qualquer forma, você pode encontrar um motivo se passar peloRejectException
rastreamento da pilha no Sentry e descobrir onde oreject()
método é chamado no Promise.fonte
Discussão com o autor dentro da seção de comentários como entrada para minha resposta:
Questão:
Resposta do autor:
De acordo com isso, aqui está minha tese:
Você tem um tempo limite dentro de um dos seus middlewares que chama guzzle. Então, vamos tentar implementar um caso reproduzível.
Aqui, temos um middleware personalizado que chama guzzle e retorna uma falha de rejeição com a mensagem de exceção da sub-chamada. É bem complicado, porque devido ao tratamento interno de erros, ele fica invisível dentro do rastreamento de pilha.
Este é um exemplo de teste de como você pode usá-lo:
Assim que eu realizar um teste contra isso, estou recebendo
Parece que a sua chamada principal do guzzle falhou, mas na realidade é a sub-chamada que falhou.
Deixe-me saber se isso ajuda você a identificar seu problema específico. Também agradeceria se você pudesse compartilhar seus middlewares para depurar isso um pouco mais.
fonte
rejection_for($e->getMessage())
vez derejection_for($e)
algum lugar nesse middleware. Eu estava olhando para a fonte original para middleware padrão (como aqui: github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106 ), mas não poderia muito dizer por que haviarejection_for($e)
em vez dethrow $e
. Parece cascata da mesma maneira, de acordo com o meu caso de teste. Veja a postagem original para um caso de teste simplificado.Olá eu não entendi se você acabou resolvendo seu problema ou não.
Bem, eu gostaria que você publicasse qual é o log de erros. Pesquise no PHP e no log de erros do seu servidor
Aguardo seu feedback
fonte
$client->request('GET', ...)
(apenas um cliente guzzle regular).Como isso acontece esporadicamente no seu ambiente e é difícil replicar o lançamento do
RejectionException
(pelo menos eu não poderia), você pode simplesmente adicionar outrocatch
bloco ao seu código, veja abaixo:Ele deve nos dar algumas idéias sobre por que e quando isso acontece.
fonte