J'ai des travaux qui s'exécutent sur plusieurs travailleurs de file d'attente, qui contiennent des requêtes HTTP utilisant Guzzle. Cependant, le bloc try-catch à l'intérieur de ce travail ne semble pas reprendre GuzzleHttp\Exception\RequestException
lorsque j'exécute ce travail en arrière-plan. Le processus en cours d'exécution est un php artisan queue:work
employé du système de file d'attente Laravel qui surveille la file d'attente et récupère les travaux.
Au lieu de cela, l'exception levée est l'une des GuzzleHttp\Promise\RejectionException
avec le message:
La promesse a été rejetée pour la raison suivante: erreur cURL 28: l'opération a expiré après 30001 millisecondes avec 0 octet reçu (voir https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Il s'agit en fait d'un déguisement GuzzleHttp\Exception\ConnectException
(voir https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 ), car si j'exécute un travail similaire dans un processus PHP normal qui est déclenché par la visite d'un URL, je reçois le ConnectException
comme prévu avec le message:
Erreur cURL 28: l'opération a expiré après 100 millisecondes avec 0 octet sur 0 reçu (voir https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Exemple de code qui déclencherait ce délai:
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.
}
Le code ci-dessus renvoie un RejectionException
ou ConnectException
lorsqu'il est exécuté dans le processus de travail, mais toujours un ConnectException
lorsqu'il est testé manuellement via le navigateur (d'après ce que je peux dire).
Donc, fondamentalement, ce que je dérive, c'est que cela RejectionException
enveloppe le message du ConnectException
, mais je n'utilise pas les fonctionnalités asynchrones de Guzzle. Mes demandes se font simplement en série. La seule chose qui diffère est que plusieurs processus PHP peuvent effectuer des appels HTTP Guzzle ou que les travaux eux-mêmes expirent (ce qui devrait entraîner une exception différente pour Laravel Illuminate\Queue\MaxAttemptsExceededException
), mais je ne vois pas comment cela provoque un comportement différent du code.
Je n'ai pas pu trouver de code à l'intérieur des packages Guzzle qui utilise php_sapi_name()
/ PHP_SAPI
(qui détermine l'interface utilisée) pour exécuter différentes choses lors de l'exécution à partir de la CLI par opposition à un déclencheur de navigateur.
tl; dr
Pourquoi Guzzle me lance-t-il RejectionException
sur mes processus de travail, mais ConnectException
sur des scripts PHP réguliers déclenchés par le navigateur?
Modifier 1
Malheureusement, je ne peux pas créer d'exemple reproductible minimal. Je vois de nombreux messages d'erreur dans mon outil de suivi des problèmes Sentry, à l'exception exacte indiquée ci-dessus. La source est indiquée comme Starting Artisan command: horizon:work
(qui est Laravel Horizon, il supervise les files d'attente Laravel). J'ai vérifié à nouveau pour voir s'il y a une différence entre les versions de PHP, mais le site Web et les processus de travail exécutent le même PHP, 7.3.14
ce qui est correct:
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
- La version cURL est
cURL 7.58.0
. - La version Guzzle est
guzzlehttp/guzzle 6.5.2
- La version Laravel est
laravel/framework 6.12.0
Edit 2 (trace de pile)
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
La Client::callRequest()
fonction contient simplement un client Guzzle sur lequel j'appelle $client->request($request['method'], $request['url'], $request['options']);
(donc je n'utilise pas requestAsync()
). Je pense que cela a quelque chose à voir avec l'exécution de travaux en parallèle qui provoque ce problème.
Edit 3 (solution trouvée)
Considérez le testcase suivant qui fait une requête HTTP (qui devrait retourner une réponse 200 régulière):
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);
}
Maintenant, ce que j'ai fait à l'origine était d'appeler, rejection_for($e->getMessage())
ce qui crée le sien en RejectionException
fonction de la chaîne de message. Appeler rejection_for($e)
était la bonne solution ici. Il ne reste plus qu'à répondre si cette rejection_for
fonction est la même qu'une simple throw $e
.
HandlerStack
?