Tengo trabajos que se ejecutan en varios trabajadores de cola, que contienen algunas solicitudes HTTP que usan Guzzle. Sin embargo, el bloque try-catch dentro de este trabajo no parece recuperarse GuzzleHttp\Exception\RequestException
cuando ejecuto este trabajo en el proceso en segundo plano. El proceso en ejecución es un php artisan queue:work
trabajador del sistema de colas Laravel que monitorea la cola y recoge los trabajos.
En cambio, la excepción que se produce es una GuzzleHttp\Promise\RejectionException
con el mensaje:
La promesa fue rechazada con razón: error 28 de cURL: La operación expiró después de 30001 milisegundos con 0 bytes recibidos (consulte https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Esto es realmente un disfraz GuzzleHttp\Exception\ConnectException
(ver https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 ), porque si ejecuto un trabajo similar en un proceso PHP normal que se activa al visitar un URL, obtengo lo ConnectException
que pretendía con el mensaje:
Error 28 de cURL: La operación expiró después de 100 milisegundos con 0 de 0 bytes recibidos (consulte https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Código de muestra que desencadenaría este tiempo de espera:
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.
}
El código anterior arroja una RejectionException
o ConnectException
cuando se ejecuta en el proceso de trabajo, pero siempre una ConnectException
cuando se prueba manualmente a través del navegador (por lo que puedo decir).
Básicamente, lo que deduzco es que esto RejectionException
está envolviendo el mensaje del ConnectException
, sin embargo, no estoy usando las características asincrónicas de Guzzle. Mis solicitudes simplemente se hacen en serie. Lo único que difiere es que múltiples procesos de PHP podrían estar haciendo llamadas HTTP de Guzzle o que los trabajos en sí mismos están agotando el tiempo (lo que debería resultar en una excepción diferente de Laravel Illuminate\Queue\MaxAttemptsExceededException
), pero no veo cómo esto hace que el código se comporte de manera diferente.
No pude encontrar ningún código dentro de los paquetes de Guzzle que está usando php_sapi_name()
/ PHP_SAPI
(que determina la interfaz utilizada) para ejecutar diferentes cosas cuando se ejecuta desde la CLI en lugar de un disparador del navegador.
tl; dr
¿Por qué Guzzle me lanza RejectionException
s en mis procesos de trabajo, pero ConnectException
s en scripts PHP normales activados a través del navegador?
Editar 1
Lamentablemente, no puedo crear un ejemplo mínimo reproducible. Veo muchos mensajes de error en mi rastreador de problemas de Sentry, con la excepción exacta que se muestra arriba. La fuente se indica como Starting Artisan command: horizon:work
(que es Laravel Horizon, supervisa las colas de Laravel). He revisado nuevamente para ver si hay una discrepancia entre las versiones de PHP, pero tanto el sitio web como los procesos de trabajo ejecutan el mismo PHP 7.3.14
que es correcto:
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 versión de cURL es
cURL 7.58.0
. - La versión de Guzzle es
guzzlehttp/guzzle 6.5.2
- La versión de Laravel es
laravel/framework 6.12.0
Editar 2 (seguimiento de pila)
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()
función contiene simplemente un Guzzle Client en el que llamo $client->request($request['method'], $request['url'], $request['options']);
(así que no estoy usando requestAsync()
). Creo que tiene algo que ver con ejecutar trabajos en paralelo que causa este problema.
Edición 3 (solución encontrada)
Considere el siguiente caso de prueba que realiza una solicitud HTTP (que debería devolver una respuesta 200 normal):
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);
}
Ahora, lo que hice originalmente fue llamar, rejection_for($e->getMessage())
que crea el suyo en RejectionException
función de la cadena del mensaje. Llamar rejection_for($e)
fue la solución correcta aquí. Lo único que queda por responder es si esta rejection_for
función es igual a una simple throw $e
.
HandlerStack
?