Ich habe Jobs, die auf mehreren Warteschlangenarbeitern ausgeführt werden und einige HTTP-Anforderungen mit Guzzle enthalten. Der Try-Catch-Block in diesem Job scheint jedoch nicht zu funktionieren, GuzzleHttp\Exception\RequestException
wenn ich diesen Job im Hintergrund ausführe. Der laufende Prozess ist ein php artisan queue:work
Laravel-Warteschlangensystemarbeiter, der die Warteschlange überwacht und die Jobs aufnimmt.
Stattdessen wird eine Ausnahme GuzzleHttp\Promise\RejectionException
mit der folgenden Meldung ausgelöst :
Das Versprechen wurde mit folgendem Grund abgelehnt: cURL-Fehler 28: Zeitüberschreitung nach 30001 Millisekunden mit 0 empfangenen Bytes (siehe https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Dies ist eigentlich eine Verkleidung GuzzleHttp\Exception\ConnectException
(siehe https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 ), denn wenn ich einen ähnlichen Job in einem regulären PHP-Prozess ausführe, der durch den Besuch eines ausgelöst wird URL, ich bekomme die ConnectException
wie vorgesehen mit der Nachricht:
cURL-Fehler 28: Zeitüberschreitung des Vorgangs nach 100 Millisekunden mit 0 von 0 empfangenen Bytes (siehe https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Beispielcode, der dieses Timeout auslösen würde:
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.
}
Der obige Code löst entweder ein RejectionException
oder ein, ConnectException
wenn es im Worker-Prozess ausgeführt wird, aber immer ein, ConnectException
wenn es manuell über den Browser getestet wird (soweit ich das beurteilen kann).
Im Grunde genommen leite ich daraus ab, dass dies RejectionException
die Nachricht von der ConnectException
umschließt, ich jedoch nicht die asynchronen Funktionen von Guzzle verwende. Meine Anfragen werden einfach in Serie gemacht. Das einzige, was sich unterscheidet, ist, dass mehrere PHP-Prozesse möglicherweise Guzzle-HTTP-Aufrufe ausführen oder dass die Jobs selbst eine Zeitüberschreitung aufweisen (was zu einer anderen Ausnahme führen sollte als bei Laravel Illuminate\Queue\MaxAttemptsExceededException
), aber ich sehe nicht, wie sich der Code dadurch anders verhält.
Ich konnte keinen Code in den Guzzle-Paketen finden, der php_sapi_name()
/ PHP_SAPI
(der die verwendete Schnittstelle bestimmt) verwendet, um andere Dinge auszuführen, wenn er über die CLI ausgeführt wird, im Gegensatz zu einem Browser-Trigger.
tl; dr
Warum wirft Guzzle mich RejectionException
auf meine Arbeitsprozesse, aber ConnectException
auf normale PHP-Skripte, die über den Browser ausgelöst werden?
Bearbeiten 1
Leider kann ich kein minimal reproduzierbares Beispiel erstellen. Ich sehe viele Fehlermeldungen in meinem Sentry Issue Tracker, mit der genauen Ausnahme, die oben gezeigt wird. Die Quelle wird angegeben als Starting Artisan command: horizon:work
(Laravel Horizon überwacht die Laravel-Warteschlangen). Ich habe erneut überprüft, ob zwischen den PHP-Versionen eine Diskrepanz besteht, aber sowohl auf der Website als auch in den Arbeitsprozessen wird dasselbe PHP ausgeführt, 7.3.14
was korrekt ist:
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
- Die cURL-Version ist
cURL 7.58.0
. - Guzzle-Version ist
guzzlehttp/guzzle 6.5.2
- Laravel Version ist
laravel/framework 6.12.0
Bearbeiten 2 (Stack-Trace)
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
Die Client::callRequest()
Funktion enthält einfach einen Guzzle Client, auf dem ich anrufe $client->request($request['method'], $request['url'], $request['options']);
(also benutze ich nicht requestAsync()
). Ich denke, es hat etwas mit dem parallelen Ausführen von Jobs zu tun, das dieses Problem verursacht.
Edit 3 (Lösung gefunden)
Betrachten Sie den folgenden Testfall, der eine HTTP-Anfrage stellt (die eine reguläre 200-Antwort zurückgeben sollte):
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);
}
Was ich ursprünglich getan habe, war ein Aufruf, rejection_for($e->getMessage())
der RejectionException
basierend auf der Nachrichtenzeichenfolge eine eigene erstellt . Anrufen rejection_for($e)
war hier die richtige Lösung. Es bleibt nur zu antworten, ob diese rejection_for
Funktion mit einer einfachen identisch ist throw $e
.
HandlerStack
?Antworten:
Hallo, ich würde gerne wissen, ob Sie Fehler 4xx oder Fehler 5xx haben
Trotzdem werde ich einige Alternativen für Lösungen nennen, die Ihrem Problem ähneln
Alternative 1
Ich möchte dieses Problem lösen. Ich hatte dieses Problem mit einem neuen Produktionsserver, der unerwartete 400 Antworten zurückgab, verglichen mit der Entwicklungs- und Testumgebung, die wie erwartet funktioniert. einfach install apt installieren php7.0-curl behoben.
Es war eine brandneue Ubuntu 16.04 LTS-Installation mit PHP, die über ppa: ondrej / php installiert wurde. Beim Debuggen stellte ich fest, dass die Header unterschiedlich waren. Beide sendeten ein mehrteiliges Formular mit eingespannten Daten, aber ohne php7.0-curl sendete es einen Connection: close-Header anstelle des Expect: 100-Continue; Beide Anforderungen hatten Transfer-Encoding: Chunked.
Alternative 2
Vielleicht solltest du das versuchen
Die Guzzle muss geschaltet werden, wenn der Antwortcode nicht 200 ist
Alternative 3
In meinem Fall, weil ich ein leeres Array in den $ options ['json'] der Anfrage übergeben hatte, konnte ich die 500 auf dem Server nicht mit Postman oder cURL reproduzieren, selbst wenn der Header Content-Type: application / json request übergeben wurde.
Das Entfernen des JSON-Schlüssels aus dem Optionsarray der Anforderung löste das Problem.
Ich habe ungefähr 30 Minuten damit verbracht herauszufinden, was falsch ist, weil dieses Verhalten sehr inkonsistent ist. Bei allen anderen Anfragen, die ich mache, verursachte das Übergeben von $ options ['json'] = [] keine Probleme. Es könnte ein Serverproblem sein, aber ich kontrolliere den Server nicht.
Senden Sie Feedback zu den erhaltenen Details
quelle
ConnectException
ist keine Antwort zugeordnet, daher gibt es meines Wissens keinen 400- oder 500-Fehler. Es sieht so aus, als ob Sie tatsächlich fangen solltenBadResponseException
(oderClientException
(4xx) /ServerException
(5xx), die beide Kinder davon sind)Guzzle verwendet Versprechen sowohl für synchrone als auch für asynchrone Anforderungen. Der einzige Unterschied besteht darin, dass bei Verwendung einer synchronen Anforderung (Ihrem Fall) diese sofort durch Aufrufen einer
wait()
Methode erfüllt wird . Beachten Sie diesen Teil:Es wird also ausgelöst,
RequestException
was eine Instanz von ist,\Exception
und es tritt immer bei 4xx- und 5xx-HTTP-Fehlern auf, es sei denn, das Auslösen von Ausnahmen ist über Optionen deaktiviert. Wie Sie sehen, kann es auch ein werfen,RejectionException
wenn der Grund keine Instanz ist,\Exception
z. B. wenn der Grund eine Zeichenfolge ist, die in Ihrem Fall vorkommt. Das Seltsame ist, dass SieRejectException
eher alsRequestException
GuzzleConnectException
auf Verbindungs-Timeout-Fehler werfen . Auf jeden Fall können Sie einen Grund finden, wenn Sie IhreRejectException
Stapelverfolgung in Sentry durchgehen und herausfinden, wo diereject()
Methode in Promise aufgerufen wird.quelle
Diskussion mit dem Autor im Kommentarbereich als Einstieg in meine Antwort:
Frage:
Antwort des Autors:
Demnach ist hier meine These:
Sie haben eine Zeitüberschreitung in einer Ihrer Middleware, die guzzle ruft. Versuchen wir also, einen reproduzierbaren Fall zu implementieren.
Hier haben wir eine benutzerdefinierte Middleware, die guzzle aufruft und einen Ablehnungsfehler mit der Ausnahmemeldung des Unteraufrufs zurückgibt. Es ist ziemlich knifflig, weil es aufgrund der internen Fehlerbehandlung im Stack-Trace unsichtbar wird.
Dies ist ein Testbeispiel, wie Sie es verwenden können:
Sobald ich einen Test dagegen durchführe, erhalte ich
Es sieht also so aus, als ob Ihr Hauptaufruf fehlgeschlagen ist, aber in Wirklichkeit ist es der Unteraufruf, der fehlgeschlagen ist.
Lassen Sie mich wissen, ob dies Ihnen hilft, Ihr spezifisches Problem zu identifizieren. Ich würde mich auch sehr freuen, wenn Sie Ihre Middlewares teilen können, um dies ein wenig weiter zu debuggen.
quelle
rejection_for($e->getMessage())
stattrejection_for($e)
irgendwo in dieser Middleware angerufen . Ich habe in der Originalquelle nach Standard-Middleware gesucht (wie hier: github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106 ), konnte aber nicht genau sagen, warum esrejection_for($e)
stattdessen gabthrow $e
. Laut meinem Testfall scheint es genauso zu kaskadieren. Im vereinfachten Test finden Sie einen vereinfachten Testfall.Hallo, ich habe nicht verstanden, ob Sie Ihr Problem gelöst haben oder nicht.
Nun, ich möchte, dass Sie das Fehlerprotokoll veröffentlichen. Suchen Sie sowohl in PHP als auch im Fehlerprotokoll Ihres Servers
Ich erwarte Ihr Feedback
quelle
$client->request('GET', ...)
auslöst, ist (nur ein normaler Guzzle-Client).Da dies in Ihrer Umgebung sporadisch vorkommt und es schwierig ist, das Auslösen des
RejectionException
(zumindest konnte ich nicht) zu replizieren , können Siecatch
Ihrem Code einfach einen weiteren Block hinzufügen , siehe unten:Es muss Ihnen und uns einige Ideen geben, warum und wann dies geschieht.
quelle