Mam zadania, które działają na wielu pracownikach kolejki, które zawierają niektóre żądania HTTP przy użyciu Guzzle. Jednak blok try-catch w tym zadaniu nie wydaje się wychwytywać, GuzzleHttp\Exception\RequestException
gdy uruchamiam je w tle. Działający proces to proces php artisan queue:work
roboczy systemu kolejek Laravel, który monitoruje kolejkę i odbiera zadania.
Zamiast tego zgłaszany jest wyjątek związany GuzzleHttp\Promise\RejectionException
z komunikatem:
Obietnica została odrzucona z uzasadnieniem: błąd cURL 28: Upłynął limit czasu operacji po 30001 milisekundach z otrzymaniem 0 bajtów (patrz https://curl.haxx.se/libcurl/c/libcurl-errors.html )
W rzeczywistości jest to ukryte GuzzleHttp\Exception\ConnectException
(patrz https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 ), ponieważ jeśli uruchomię podobne zadanie w zwykłym procesie PHP, który jest wywoływany przez odwiedziny w URL, otrzymuję ConnectException
zgodnie z przeznaczeniem z komunikatem:
błąd cURL 28: Upłynął limit czasu operacji po 100 milisekundach z otrzymaniem 0 z 0 bajtów (patrz https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Przykładowy kod, który wyzwoli ten limit czasu:
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.
}
Powyższy kod wyrzuca albo a RejectionException
lub ConnectException
gdy jest uruchamiany w procesie roboczym, ale zawsze a, ConnectException
gdy jest testowany ręcznie przez przeglądarkę (z tego, co mogę powiedzieć).
Więc w zasadzie czerpię to, że zawijam RejectionException
wiadomość od ConnectException
, jednak nie używam asynchronicznych funkcji Guzzle. Moje prośby są po prostu realizowane szeregowo. Jedyną różnicą jest to, że wiele procesów PHP może wykonywać połączenia Guzzle HTTP lub że same zadania wygasają (co powinno skutkować innym wyjątkiem należącym do Laravela Illuminate\Queue\MaxAttemptsExceededException
), ale nie rozumiem, jak to powoduje, że kod zachowuje się inaczej.
Nie mogłem znaleźć żadnego kodu w pakietach Guzzle, który używa php_sapi_name()
/ PHP_SAPI
(który określa używany interfejs) do wykonywania różnych rzeczy podczas uruchamiania z CLI, w przeciwieństwie do wyzwalacza przeglądarki.
tl; dr
Dlaczego Guzzle rzuca mi RejectionException
procesy robocze , ConnectException
a zwykłe skrypty PHP uruchamiane przez przeglądarkę?
Edytuj 1
Niestety nie mogę stworzyć minimalnego odtwarzalnego przykładu. Widzę wiele komunikatów o błędach w moim narzędziu do śledzenia problemów Sentry, z dokładnym wyjątkiem pokazanym powyżej. Źródło jest określone jako Starting Artisan command: horizon:work
(którym jest Laravel Horizon, nadzoruje kolejki Laravel). Ponownie sprawdziłem, czy istnieje rozbieżność między wersjami PHP, ale zarówno witryna, jak i procesy robocze działają w tym samym PHP, 7.3.14
co jest poprawne:
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
- Wersja cURL to
cURL 7.58.0
. - Wersja z wylotem jest
guzzlehttp/guzzle 6.5.2
- Wersja Laravela to
laravel/framework 6.12.0
Edycja 2 (ślad stosu)
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
Ta Client::callRequest()
funkcja zawiera po prostu klienta Guzzle, do którego dzwonię $client->request($request['method'], $request['url'], $request['options']);
(więc nie używam requestAsync()
). Myślę, że ma to związek z równoległym wykonywaniem zadań, które powodują ten problem.
Edytuj 3 (znaleziono rozwiązanie)
Rozważ następującą walizkę testową, która wysyła żądanie HTTP (które powinno zwrócić regularną odpowiedź 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);
}
Teraz to, co pierwotnie zrobiłem, to wywołanie, rejection_for($e->getMessage())
które tworzy własne RejectionException
na podstawie ciągu komunikatu. Dzwonienie rejection_for($e)
było tutaj poprawnym rozwiązaniem. Pozostaje tylko odpowiedzieć, jeśli ta rejection_for
funkcja jest taka sama jak prosta throw $e
.
HandlerStack
?