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
?Réponses:
Bonjour, je voudrais savoir si vous rencontrez une erreur 4xx ou une erreur 5xx
Mais même ainsi, je vais mettre quelques alternatives pour les solutions trouvées qui ressemblent à votre problème
alternative 1
J'aimerais contourner cela, j'ai eu ce problème avec un nouveau serveur de production retournant 400 réponses inattendues par rapport à l'environnement de développement et de test fonctionnant comme prévu; simplement installer apt install php7.0-curl l'a corrigé.
C'était une toute nouvelle installation Ubuntu 16.04 LTS avec php installé via ppa: ondrej / php, pendant le débogage, j'ai remarqué que les en-têtes étaient différents. Tous deux envoyaient un formulaire en plusieurs parties avec des données bloquées, mais sans php7.0-curl, il envoyait un en-tête Connection: close plutôt que Expect: 100-Continue; dont les deux demandes avaient Transfer-Encoding: chunked.
alternative 2
Vous devriez peut-être essayer ceci
Guzzle besoin de cactching si le code de réponse n'est pas 200
alternative 3
Dans mon cas, c'était parce que j'avais passé un tableau vide dans les options $ de la requête ['json'] Je n'ai pas pu reproduire le 500 sur le serveur en utilisant Postman ou cURL même en passant l'en-tête de requête Content-Type: application / json.
Quoi qu'il en soit, la suppression de la clé json du tableau d'options de la requête a résolu le problème.
J'ai passé environ 30 minutes à essayer de comprendre ce qui ne va pas car ce comportement est très incohérent. Pour toutes les autres demandes que je fais, le passage de $ options ['json'] = [] n'a causé aucun problème. Ce pourrait être un problème de serveur, je ne contrôle pas le serveur.
envoyer des commentaires sur les détails obtenus
la source
ConnectException
n'a pas de réponse associée, donc il n'y a donc pas d'erreur 400 ou 500 à ma connaissance. Il semble que vous devriez réellement attraperBadResponseException
(ouClientException
(4xx) /ServerException
(5xx) qui sont tous les deux des enfants)Guzzle utilise Promises pour les demandes synchrones et asynchrones. La seule différence est que lorsque vous utilisez une demande synchrone (votre cas) - elle est satisfaite immédiatement en appelant une
wait()
méthode . Notez cette partie:Ainsi, il lance
RequestException
ce qui est une instance de\Exception
et cela se produit toujours sur les erreurs HTTP 4xx et 5xx, sauf si le lancement d'exceptions est désactivé via les options. Comme vous le voyez, il peut également lancer unRejectionException
si la raison n'est pas une instance de\Exception
par exemple si la raison est une chaîne qui semble se produire dans votre cas. La chose étrange est que vous obtenezRejectException
plutôt queRequestException
lorsque Guzzle lance uneConnectException
erreur de dépassement de délai de connexion. Quoi qu'il en soit, vous pouvez trouver une raison si vous parcourez votreRejectException
trace de pile dans Sentry et trouvez où lareject()
méthode est appelée sur Promise.la source
Discussion avec l'auteur à l'intérieur de la section des commentaires pour commencer ma réponse:
Question:
Réponse de l'auteur:
Selon ceci voici ma thèse:
Vous avez un délai d'attente à l'intérieur de l'un de vos middleware qui appelle guzzle. Essayons donc d'implémenter un cas reproductible.
Ici, nous avons un middleware personnalisé qui appelle guzzle et renvoie un échec de rejet avec le message d'exception du sous-appel. C'est assez délicat, car en raison de la gestion interne des erreurs, il devient invisible à l'intérieur du stack-trace.
Voici un exemple de test sur la façon dont vous pouvez l'utiliser:
Dès que je fais un test contre cela, je reçois
Il semble donc que votre appel principal ait échoué, mais en réalité, c'est le sous-appel qui a échoué.
Faites-moi savoir si cela vous aide à identifier votre problème spécifique. J'apprécierais également beaucoup que vous puissiez partager vos middlewares afin de déboguer un peu plus loin.
la source
rejection_for($e->getMessage())
au lieu derejection_for($e)
quelque part dans ce middleware. Je cherchais la source d'origine du middleware par défaut (comme ici: github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106 ), mais je ne pouvais pas vraiment dire pourquoi il y en avaitrejection_for($e)
au lieu dethrow $e
. Il semble en cascade de la même manière selon mon testcase. Voir le post d'origine pour un testcase simplifié.Bonjour, je n'ai pas compris si vous avez fini par résoudre votre problème ou non.
Eh bien, j'aimerais que vous publiez le journal des erreurs. Recherchez à la fois en PHP et dans le journal des erreurs de votre serveur
J'attends vos commentaires
la source
$client->request('GET', ...)
(juste un client habituel).Comme cela se produit sporadiquement sur votre environnement et qu'il est difficile de répliquer en lançant le
RejectionException
(du moins je ne pouvais pas), pouvez-vous simplement ajouter un autrecatch
bloc à votre code, voir ci-dessous:Il doit vous donner, ainsi qu'à nous, quelques idées sur les raisons et le moment où cela se produit.
la source