web-dev-qa-db-fra.com

Guzzle lançant une exception de rejet au lieu de ConnectException sur le processus d'arrière-plan

J'ai des travaux qui s'exécutent sur plusieurs travailleurs de file d'attente, qui contiennent des requêtes HTTP à l'aide de Guzzle. Cependant, le bloc try-catch à l'intérieur de cette tâche ne semble pas prendre GuzzleHttp\Exception\RequestException Lorsque j'exécute ces tâches dans le processus d'arrière-plan. Le processus en cours d'exécution est un php artisan queue:work Qui est un Laravel file worker system) qui surveille la file d'attente et récupère les travaux.

Au lieu de cela, l'exception qui est levée est l'une des GuzzleHttp\Promise\RejectionException Avec le message:

La promesse a été rejetée avec raison: 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 GuzzleHttp\Exception\ConnectException Déguisé (voir https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 ), car si je lance un travail similaire dans un processus normal PHP qui est déclenché en visitant une URL, j'obtiens 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 jette 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éduis, c'est que ce 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 PHP peuvent faire des appels HTTP Guzzle ou que les travaux eux-mêmes expirent (ce qui devrait entraîner une exception différente étant Illuminate\Queue\MaxAttemptsExceededException) , mais je ne vois pas comment cela fait que le code se comporte différemment.

Je n'ai pas trouvé de code dans les 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 RejectionExceptions sur mes processus de travail, mais ConnectExceptions sur des scripts PHP déclenchés via le navigateur?)

Modifier 1

Malheureusement, je ne peux pas créer un 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 Laravel files d'attente). J'ai vérifié à nouveau pour voir si il y a un écart entre PHP versions, 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 fonction Client::callRequest() 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 cause ce problème.

Edit 3 (solution trouvée)

Considérez le cas de test suivant qui fait une requête HTTP (qui devrait renvoyer une réponse standard 200):

        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()) qui crée son propre RejectionException basé sur la chaîne de message. Appeler rejection_for($e) était la bonne solution ici. Il ne reste plus qu'à répondre si cette fonction rejection_for Est identique à un simple throw $e.

9
Flame

Discussion avec l'auteur à l'intérieur de la section des commentaires pour commencer ma réponse:

Question:

Avez-vous mis en place un middleware guzzle personnalisé (indice: HandlerStack)?

Réponse de l'auteur:

Oui divers. Mais le middleware est fondamentalement un modificateur de requête/réponse, même les requêtes guzzle que j'y fais sont effectuées de manière synchrone.


D'après ceci, voici ma thèse:

Vous avez un timeout dans 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 des erreurs internes, il est invisible à l'intérieur de la trace de pile.

function custom_middleware(string $baseUri = 'http://127.0.0.1:8099', float $timeout = 0.2)
{
    return function (callable $handler) use ($baseUri, $timeout) {
        return function ($request, array $options) use ($handler, $baseUri, $timeout) {
            try {
                $client = new GuzzleHttp\Client(['base_uri' => $baseUri, 'timeout' => $timeout,]);
                $client->get('/a');
            } catch (Exception $exception) {
                return \GuzzleHttp\Promise\rejection_for($exception->getMessage());
            }
            return $handler($request, $options);
        };
    };
}

Voici un exemple de test comment vous pouvez l'utiliser:

$baseUri = 'http://127.0.0.1:8099'; // php -S 127.0.0.1:8099 test.php << includes a simple sleep(10); statement
$timeout = 0.2;

$handler = \GuzzleHttp\HandlerStack::create();
$handler->Push(custom_middleware($baseUri, $timeout));

$client = new Client([
    'handler' => $handler,
    'base_uri' => $baseUri,
]);

try {
    $response = $client->get('/b');
} catch (Exception $exception) {
    var_dump(get_class($exception), $exception->getMessage());
}

Dès que j'effectue un test contre cela, je reçois

$ php test2.php 
string(37) "GuzzleHttp\Promise\RejectionException"
string(174) "The promise was rejected with reason: cURL error 28: Operation timed out after 202 milliseconds with 0 bytes received (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)"

Il semble donc que votre appel de guzzle principal a é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 si vous pouviez partager vos middlewares afin de déboguer un peu plus cela.

1
Christoph Kluge

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 pourrais pas), pouvez-vous simplement ajouter un autre bloc catch à votre code, voir ci-dessous:

try {
    $c = new \GuzzleHttp\Client([
        'timeout' => 0.1
    ]);
    $response = (string) $c->get('https://example.com')->getBody();
} catch (GuzzleHttp\Promise\RejectionException $e) {
    // Log the output of $e->getTraceAsString();
} 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.
}

Cela doit vous donner, à vous et à nous, des idées sur pourquoi et quand cela se produit.

0
Vladimir

Bonjour, je n'ai pas compris si vous avez fini par résoudre votre problème ou non.

Eh bien, je voudrais que vous postez quel est le journal des erreurs. Recherchez à la fois dans PHP et dans le journal des erreurs de votre serveur

J'attends vos commentaires

0
PauloBoaventura