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
?Respuestas:
Hola, me gustaría saber si tienes el error 4xx o el error 5xx
Pero aun así, pondré algunas alternativas para las soluciones encontradas que se parecen a su problema
alternativa 1
Me gustaría resolver esto, tuve este problema con un nuevo servidor de producción que devolvió 400 respuestas inesperadas en comparación con el entorno de desarrollo y prueba que funciona como se esperaba; simplemente instalando apt install php7.0-curl lo arregló.
Fue una nueva instalación de Ubuntu 16.04 LTS con php instalado a través de ppa: ondrej / php, durante la depuración noté que los encabezados eran diferentes. Ambos enviaban un formulario de varias partes con datos arrojados, sin embargo, sin php7.0-curl estaba enviando una conexión: cerrar encabezado en lugar de esperar: 100-continuar; ambas solicitudes tenían Transfer-Encoding: fragmentado.
alternativa 2
Tal vez deberías probar esto
Guzzle necesita cactching si el código de respuesta no es 200
alternativa 3
En mi caso fue porque había pasado una matriz vacía en la opción $ options ['json'] de la solicitud. No pude reproducir las 500 en el servidor usando Postman o cURL, incluso al pasar el encabezado de solicitud Content-Type: application / json.
De todos modos, eliminar la clave json de la matriz de opciones de la solicitud resolvió el problema.
Pasé como 30 minutos tratando de descubrir qué está mal porque este comportamiento es muy inconsistente. Para todas las demás solicitudes que estoy haciendo, pasar $ options ['json'] = [] no causó ningún problema. Podría ser un problema del servidor, aunque no controlo el servidor.
enviar comentarios sobre los detalles obtenidos
fuente
ConnectException
no tiene una respuesta asociada, por lo que no hay un error de 400 o 500 hasta donde yo sé. Parece que en realidad deberías estar atrapandoBadResponseException
(oClientException
(4xx) /ServerException
(5xx) que son hijos de ella)Guzzle utiliza Promesas para solicitudes sincrónicas y asincrónicas. La única diferencia es que cuando usa una solicitud síncrona (su caso), se cumple de inmediato llamando a un
wait()
método . Tenga en cuenta esta parte:Por lo tanto, arroja
RequestException
una instancia de\Exception
y siempre ocurre en errores HTTP 4xx y 5xx, a menos que se desactiven las excepciones a través de las opciones. Como puede ver, también puede arrojar unRejectionException
si el motivo no es una instancia de,\Exception
por ejemplo, si el motivo es una cadena que parece suceder en su caso. Lo extraño es que obtienesRejectException
más queRequestException
cuando Guzzle lanzaConnectException
un error de tiempo de espera de conexión. De todos modos, puede encontrar una razón si revisa suRejectException
seguimiento de pila en Sentry y encuentra dóndereject()
se llama al método en Promise.fuente
Discusión con el autor dentro de la sección de comentarios como iniciador de mi respuesta:
Pregunta:
Respuesta del autor:
De acuerdo con esto aquí está mi tesis:
Tienes un tiempo de espera dentro de uno de tus middleware que llama guzzle. Así que intentemos implementar un caso reproducible.
Aquí tenemos un middleware personalizado que llama a guzzle y devuelve un error de rechazo con el mensaje de excepción de la sub-llamada. Es bastante complicado, porque debido al manejo interno de errores se vuelve invisible dentro del seguimiento de la pila.
Este es un ejemplo de prueba de cómo puedes usarlo:
Tan pronto como realizo una prueba contra esto, estoy recibiendo
Por lo tanto, parece que su llamada principal ha fallado, pero en realidad es la sub-llamada que falló.
Avíseme si esto le ayuda a identificar su problema específico. También agradecería mucho que pueda compartir sus middlewares para depurar esto un poco más.
fuente
rejection_for($e->getMessage())
lugar enrejection_for($e)
lugar de en ese middleware. Estaba buscando la fuente original para el middleware predeterminado (como aquí: github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106 ), pero no podía decir por qué había enrejection_for($e)
lugar dethrow $e
. Parece en cascada de la misma manera según mi caso de prueba. Vea la publicación original para un caso de prueba simplificado.Hola, no entendí si terminaste resolviendo tu problema o no.
Bueno, me gustaría que publicaras cuál es el registro de errores. Busque tanto en PHP como en el registro de errores de su servidor
Espero sus comentarios
fuente
$client->request('GET', ...)
(solo un cliente habitual).Como esto sucede esporádicamente en su entorno y es difícil de replicar arrojando el
RejectionException
(al menos no pude), ¿puede agregar otrocatch
bloque a su código?Debe darles a usted y a nosotros algunas ideas sobre por qué y cuándo sucede esto.
fuente