Ho lavori eseguiti su più operatori di coda, che contengono alcune richieste HTTP tramite Guzzle. Tuttavia, il blocco try-catch all'interno di questo lavoro non sembra sollevarsi GuzzleHttp\Exception\RequestExceptionquando eseguo questi lavori nel processo in background. Il processo in esecuzione è un php artisan queue:worklavoratore del sistema di coda Laravel che controlla la coda e preleva i lavori.
Invece, l'eccezione generata è una delle seguenti GuzzleHttp\Promise\RejectionExceptioncon il messaggio:
La promessa è stata respinta con motivo: errore cURL 28: operazione scaduta dopo 30001 millisecondi con 0 byte ricevuti (vedere https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Questo è in realtà un travestimento GuzzleHttp\Exception\ConnectException(vedi https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 ), perché se eseguo un lavoro simile in un normale processo PHP che viene attivato visitando un URL, ottengo ConnectExceptioncome previsto con il messaggio:
Errore cURL 28: Operazione scaduta dopo 100 millisecondi con 0 byte su 0 ricevuti (vedere https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Codice di esempio che attiva questo timeout:
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.
}
Il codice sopra genera un RejectionExceptiono ConnectExceptionquando viene eseguito nel processo di lavoro, ma sempre un ConnectExceptionquando testato manualmente attraverso il browser (da quello che posso dire).
Quindi, in sostanza, ciò che desidero è che questo RejectionExceptionsta avvolgendo il messaggio dal ConnectException, tuttavia non sto usando le funzionalità asincrone di Guzzle. Le mie richieste sono semplicemente fatte in serie. L'unica cosa che differisce è che più processi PHP potrebbero effettuare chiamate Guzzle HTTP o che i lavori stessi stanno scadendo (il che dovrebbe comportare un'eccezione diversa da quella di Laravel Illuminate\Queue\MaxAttemptsExceededException), ma non vedo come ciò causi un comportamento diverso del codice.
Non sono riuscito a trovare alcun codice all'interno dei pacchetti Guzzle che utilizza php_sapi_name()/ PHP_SAPI(che determina l'interfaccia utilizzata) per eseguire cose diverse quando si esegue dalla CLI anziché un trigger del browser.
tl; dr
Perché Guzzle mi lancia RejectionExceptionsui miei processi di lavoro, ma ConnectExceptionsu normali script PHP attivati dal browser?
Modifica 1
Purtroppo non riesco a creare un esempio riproducibile minimo. Vedo molti messaggi di errore nel mio tracker dei problemi di Sentry, con l'esatta eccezione mostrata sopra. La fonte è dichiarata come Starting Artisan command: horizon:work(che è Laravel Horizon, supervisiona le code di Laravel). Ho controllato di nuovo per vedere se c'è una discrepanza tra le versioni di PHP, ma sia il sito Web che i processi di lavoro eseguono lo stesso PHP 7.3.14che è corretto:
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 versione cURL è
cURL 7.58.0. - La versione di Guzzle è
guzzlehttp/guzzle 6.5.2 - La versione di Laravel è
laravel/framework 6.12.0
Modifica 2 (traccia stack)
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()funzione contiene semplicemente un client Guzzle su cui chiamo $client->request($request['method'], $request['url'], $request['options']);(quindi non sto usando requestAsync()). Penso che abbia qualcosa a che fare con l'esecuzione di lavori in parallelo che causa questo problema.
Modifica 3 (soluzione trovata)
Considera il seguente testcase che effettua una richiesta HTTP (che dovrebbe restituire una risposta 200 regolare):
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);
}
Ora quello che ho fatto inizialmente era chiamare rejection_for($e->getMessage())che creava il proprio RejectionExceptionsulla base della stringa di messaggio. Chiamare rejection_for($e)era la soluzione corretta qui. L'unica cosa che resta da rispondere è se questa rejection_forfunzione è uguale a una semplice throw $e.
HandlerStack?