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
?Odpowiedzi:
Witam Chciałbym wiedzieć, czy masz błąd 4xx lub błąd 5xx
Ale i tak przedstawię alternatywne rozwiązania, które będą podobne do twojego problemu
alternatywa 1
Chciałbym to podnieść, miałem ten problem z nowym serwerem produkcyjnym zwracającym nieoczekiwane 400 odpowiedzi w porównaniu ze środowiskiem programistycznym i testowym działającym zgodnie z oczekiwaniami; po prostu zainstalowanie apt install php7.0-curl to naprawiło.
Była to zupełnie nowa instalacja Ubuntu 16.04 LTS z php zainstalowanym przez ppa: ondrej / php, podczas debugowania zauważyłem, że nagłówki są różne. Obaj wysyłali wieloczęściowy formularz z obrzuconymi danymi, jednak bez php7.0-curl wysyłał nagłówek Connection: close zamiast oczekiwanego: 100-Kontynuuj; oba żądania miały kodowanie transferowe: porcje.
alternatywa 2
Może powinieneś tego spróbować
Kaganiec wymaga kaktusa, jeśli kod odpowiedzi nie jest równy 200
alternatywa 3
W moim przypadku było to spowodowane tym, że przekazałem pustą tablicę w opcjach $ żądania [„json”] Nie mogłem odtworzyć 500 na serwerze za pomocą Postmana lub cURL, nawet po przekazaniu nagłówka żądania Content-Type: application / json.
W każdym razie usunięcie klucza json z tablicy opcji żądania rozwiązało problem.
Spędziłem około 30 minut, próbując dowiedzieć się, co jest nie tak, ponieważ takie zachowanie jest bardzo niespójne. W przypadku wszystkich innych żądań przekazanie $ options ['json'] = [] nie spowodowało żadnych problemów. Może to być problem z serwerem, chociaż nie kontroluję serwera.
wyślij opinię na temat uzyskanych szczegółów
źródło
ConnectException
nie ma powiązanej odpowiedzi, więc o ile mi wiadomo, nie ma błędu 400 ani 500. Wygląda na to, że powinieneś łapaćBadResponseException
(lubClientException
(4xx) /ServerException
(5xx), które są oboje jego dziećmi)Guzzle używa obietnic zarówno dla żądań synchronicznych, jak i asynchronicznych. Jedyna różnica polega na tym, że gdy korzystasz z żądania synchronicznego (swojego przypadku) - jest ono realizowane od razu przez wywołanie
wait()
metody . Zwróć uwagę na tę część:Zgłasza to,
RequestException
co jest wystąpieniem\Exception
i zawsze dzieje się na błędach HTTP 4xx i 5xx, chyba że wyjątki zgłaszania są wyłączone przez opcje. Jak widzisz, może również rzucić a,RejectionException
jeśli przyczyna nie jest instancją,\Exception
np. Jeśli przyczyną jest ciąg znaków, który wydaje się zdarzać w twoim przypadku. Dziwną rzeczą jest to, że dostajesz,RejectException
a nieRequestException
Guzzle rzucaConnectException
błąd przekroczenia limitu czasu połączenia. W każdym razie możesz znaleźć powód, jeśli przejrzyszRejectException
ślad stosu w Sentry i dowiesz się, gdziereject()
metoda jest wywoływana w Promise.źródło
Dyskusja z autorem w sekcji komentarzy na początek mojej odpowiedzi:
Pytanie:
Odpowiedź autora:
Zgodnie z tym oto moja teza:
Masz limit czasu w jednym z oprogramowania pośredniego, które nazywa się żubrem. Spróbujmy więc zaimplementować odtwarzalny przypadek.
Tutaj mamy niestandardowe oprogramowanie pośrednie, które wywołuje guzzle i zwraca błąd odrzucenia z komunikatem wyjątku pod-wywołania. Jest to dość trudne, ponieważ ze względu na wewnętrzną obsługę błędów staje się niewidoczne wewnątrz śledzenia stosu.
To jest przykładowy sposób, w jaki możesz go użyć:
Jak tylko przeprowadzę test na to, otrzymuję
Wygląda więc na to, że twoje główne połączenie nie powiodło się, ale w rzeczywistości jest to pod-połączenie, które się nie powiodło.
Daj mi znać, jeśli to pomoże Ci zidentyfikować konkretny problem. Byłbym również bardzo wdzięczny, jeśli możesz udostępnić swoje oprogramowanie pośrednie w celu dalszego debugowania tego.
źródło
rejection_for($e->getMessage())
zamiastrejection_for($e)
gdzieś w tym oprogramowaniu pośrednim. Szukałem na oryginalnego źródła dla domyślnego oprogramowania pośredniczącego (jak tutaj: github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106 ), ale nie mogliśmy dość powiedzieć, dlaczego tamrejection_for($e)
zamiastthrow $e
. Zdaje się, że kaskada przebiega w ten sam sposób, zgodnie z moim testem. Zobacz oryginalny post dla uproszczonej skrzynki testowej.Witaj Nie rozumiem, czy udało Ci się rozwiązać problem, czy nie.
Chciałbym, abyś opublikował dziennik błędów. Wyszukaj zarówno w PHP, jak i w dzienniku błędów serwera
Czekam na twoją opinię
źródło
$client->request('GET', ...)
(tylko zwykły klient żubra).Ponieważ zdarza się to sporadycznie w twoim środowisku i trudno jest replikować rzucanie
RejectionException
(przynajmniej nie mogłem), możesz po prostu dodać kolejnycatch
blok do swojego kodu, patrz poniżej:Musi dać nam i tobie kilka pomysłów na to, dlaczego i kiedy to się dzieje.
źródło