バックグラウンドプロセスでConnectionExceptionではなくRejectionExceptionをスローするGuzzle


9

複数のキューワーカーで実行されるジョブがあり、Guzzleを使用したHTTPリクエストがいくつか含まれています。ただし、GuzzleHttp\Exception\RequestExceptionこれらのジョブをバックグラウンドプロセスで実行しているときに、このジョブ内のtry-catchブロックが取得されないようです。実行中のプロセスは、php artisan queue:workキューを監視してジョブを取得するLaravelキューシステムワーカーです。

代わりに、スローされる例外GuzzleHttp\Promise\RejectionExceptionはメッセージの1つです。

プロミスは次の理由で拒否されました:cURLエラー28:受信した0バイトで30001ミリ秒後に操作がタイムアウトしました(https://curl.haxx.se/libcurl/c/libcurl-errors.htmlを参照 )

これは実際には偽装されていますGuzzleHttp\Exception\ConnectExceptionhttps://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22を参照)。これは、通常のPHPプロセスで同様のジョブを実行すると、 URL、私はConnectExceptionメッセージで意図したとおりに取得します:

cURLエラー28:100ミリ秒後に操作がタイムアウトし、0バイトのうち0バイトが受信されました(https://curl.haxx.se/libcurl/c/libcurl-errors.htmlを参照 )

このタイムアウトをトリガーするサンプルコード:

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.
}

上記のコードは、ワーカープロセスで実行された場合、RejectionExceptionまたはConnectException実行された場合にスローされますがConnectException、ブラウザーを介して手動でテストされた場合(私がわかることから)は常にです。

つまり、基本的に私が導き出したのは、これRejectionExceptionがからのメッセージをラップするConnectExceptionことですが、Guzzleの非同期機能を使用していません。私のリクエストは単純に連続して行われます。唯一の違いは、複数のPHPプロセスがGuzzle HTTP呼び出しを行っているか、ジョブ自体がタイムアウトしていることです(これにより、Laravelの場合とは異なる例外が発生するはずですIlluminate\Queue\MaxAttemptsExceededException)。ただし、これによってコードの動作が異なるのはわかりません。

ブラウザのトリガーではなく、CLIから実行すると、php_sapi_name()/ PHP_SAPI(使用するインターフェイスを決定する)を使用して別のものを実行するコードがGuzzleパッケージ内に見つかりませんでした。

tl; dr

Guzzle RejectionExceptionがワーカープロセスをスローするのに、ConnectExceptionブラウザーを介してトリガーされる通常のPHPスクリプトをスローするのはなぜですか?

編集1

残念ながら、最小限の再現可能な例を作成することはできません。Sentryの課題追跡に多くのエラーメッセージが表示されますが、上記とまったく同じです。ソースはStarting Artisan command: horizon:work(Laravel Horizo​​nであり、Laravelキューを監視します)と記述されています。PHPのバージョンに矛盾がないかどうかをもう一度確認しましたが、Webサイトとワーカープロセスの両方が同じPHP 7.3.14を実行しています。これは正しいことです。

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
  • cURLのバージョンはcURL 7.58.0です。
  • ガズル版は guzzlehttp/guzzle 6.5.2
  • Laravelバージョンは laravel/framework 6.12.0

編集2(スタックトレース)

    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

Client::callRequest()この関数は単に私が呼んでがつがつ食うクライアントが含まれ$client->request($request['method'], $request['url'], $request['options']);(イムは使用しないようにrequestAsync())。この問題を引き起こすのは、ジョブの並列実行と関係があると思います。

編集3(ソリューションが見つかりました)

HTTPリクエスト(通常の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);
        }

今私が最初に行ったのは、メッセージ文字列に基づいてrejection_for($e->getMessage())独自RejectionExceptionに作成する呼び出しでした。rejection_for($e)ここでの呼び出しは正しい解決策でした。このrejection_for関数が単純なものと同じであるかどうかだけが答えるべきthrow $eです。


どのGuzzleバージョンを使用していますか?
ウラジミール

1
laravelにはどのキュードライバーを使用しますか?インスタンスごとまたはインスタンスごとに何人のワーカーが並行して実行されていますか?カスタムのguzzleミドルウェアがありHandlerStackますか(ヒント:)?
Christoph Kluge

Sentryからのスタックトレースを提供できますか?
ウラジミール

@Vladimir iveがスタックトレースを追加しました。私はそれがあなたを大いに助けるとは思わない。Guzzle(および一般にPHP)でpromiseを実装する方法は読みにくいものです。
フレーム

1
@Flameで、サブガズルリクエストを実行するミドルウェアを共有できますか?問題はあると思います。その間、論文に再現可能な答えを追加します。
Christoph Kluge

回答:


3

こんにちは私はあなたがエラー4xxまたはエラー5xxを持っているかどうか知りたいです

しかし、それでも私はあなたの問題に似ている発見された解決策のいくつかの代替案を置きます

代替1

私はこれをぶつけたいのですが、開発とテスト環境が期待どおりに動作しているのに比べて、新しい本番サーバーが予期しない400応答を返すという問題がありました。apt install php7.0-curlをインストールするだけで修正されました。

これは真新しいUbuntu 16.04 LTSインストールで、phpはppa:ondrej / phpを介してインストールされました。デバッグ中に、ヘッダーが異なることに気付きました。両方ともチャックされたデータを含むマルチパートフォームを送信していましたが、php7.0-curlがなければ、Expect:100-ContinueではなくConnection:closeヘッダーを送信していました。どちらのリクエストにもTransfer-Encoding:チャンクがありました。

  代替案2

多分これを試すべきです

try {
$client = new Client();
$guzzleResult = $client->put($url, [
    'body' => $postString
]);
} catch (\GuzzleHttp\Exception\RequestException $e) {
$guzzleResult = $e->getResponse();
}

var_export($guzzleResult->getStatusCode());
var_export($guzzleResult->getBody());

応答コードが200でない場合、ガズルはキャッチングを必要とします

代替3

私の場合は、リクエストの$ options ['json']で空の配列を渡したためです。Content-Type:application / jsonリクエストヘッダーを渡しても、PostmanまたはcURLを使用してサーバーで500を再現できませんでした。

とにかく、リクエストのオプション配列からjsonキーを削除することで問題は解決しました。

この動作には一貫性がないため、何が問題なのかを理解するために30分ほど費やしました。私が行っている他のすべてのリクエストでは、$ options ['json'] = []を渡しても問題は発生しませんでした。サーバーの問題かもしれませんが、私はサーバーを制御していません。

取得した詳細についてフィードバックを送信する


良い...より速く、より正確な答えを得るために。率先して質問をGitHubのプロジェクトページに投稿しました。私はあなたが気にしないことを望む github.com/guzzle/guzzle/issues/2599
PauloBoaventura

1
aにConnectExceptionは関連する応答がないため、私が知る限り、400または500エラーはありません。実際にキャッチしているようですBadResponseException(またはClientException(4xx)/ ServerException(5xx)は両方ともその子です)
Flame


2

Guzzleは、同期リクエストと非同期リクエストの両方にPromiseを使用します。唯一の違いは、同期リクエスト(あなたのケース)を使用する場合- wait() メソッドを呼び出すことですぐに実行されます。この部分に注意してください:

wait拒否されたpromiseを呼び出すと、例外がスローされます。拒否の理由が\Exceptionその理由のインスタンスである場合、スローされます。それ以外の場合GuzzleHttp\Promise\RejectionException はがスローされgetReason 、例外のメソッドを呼び出すことで理由を取得できます。

そのため、オプションで例外のスローを無効にしない限り、RequestExceptionどちらがのインスタンスであるかをスローし、\Exception常に4xxおよび5xx HTTPエラーで発生します。ご覧のRejectionExceptionように、理由がインスタンスではない場合、\Exceptionたとえば、理由があなたのケースで発生しているように見える文字列である場合にも、それがスローされることがあります。奇妙なことに、Guzzleが接続タイムアウトエラーをスローするのRejectExceptionではなく、エラーが発生します。とにかく、Sentryでスタックトレースを調べて、Promiseでメソッドが呼び出される場所を見つけると、理由がわかる場合があります。RequestExceptionConnectExceptionRejectExceptionreject()


1

私の答えへのスターターとしてコメントセクション内の著者との議論:

質問:

カスタムguzzleミドルウェアが用意されていますか(ヒント:HandlerStack)?

著者の答え:

はい、いろいろ。しかし、ミドルウェアは基本的に要求/応答修飾子であり、そこで行うガズル要求も同期的に行われます。


これによると、ここに私の論文があります:

ミドルウェアの1つの内部でタイムアウトが発生し、それがガズルを呼び出します。それでは、再現可能なケースを実装してみましょう。

ここに、guzzleを呼び出し、サブコールの例外メッセージで拒否の失敗を返すカスタムミドルウェアがあります。内部エラー処理のためにスタックトレース内で非表示になるため、これはかなりトリッキーです。

function custom_middleware(string $baseUri = 'http://127.0.0.1:8099', float $timeout = 0.2)
{
    return function (callable $handler) use ($baseUri, $timeout) {
        return function ($request, array $options) use ($handler, $baseUri, $timeout) {
            try {
                $client = new GuzzleHttp\Client(['base_uri' => $baseUri, 'timeout' => $timeout,]);
                $client->get('/a');
            } catch (Exception $exception) {
                return \GuzzleHttp\Promise\rejection_for($exception->getMessage());
            }
            return $handler($request, $options);
        };
    };
}

これは、使用方法のテスト例です。

$baseUri = 'http://127.0.0.1:8099'; // php -S 127.0.0.1:8099 test.php << includes a simple sleep(10); statement
$timeout = 0.2;

$handler = \GuzzleHttp\HandlerStack::create();
$handler->push(custom_middleware($baseUri, $timeout));

$client = new Client([
    'handler' => $handler,
    'base_uri' => $baseUri,
]);

try {
    $response = $client->get('/b');
} catch (Exception $exception) {
    var_dump(get_class($exception), $exception->getMessage());
}

これに対してテストを実行するとすぐに、私は受け取ります

$ php test2.php 
string(37) "GuzzleHttp\Promise\RejectionException"
string(174) "The promise was rejected with reason: cURL error 28: Operation timed out after 202 milliseconds with 0 bytes received (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)"

したがって、メインのguzzle呼び出しが失敗したように見えますが、実際には、失敗したのはサブ呼び出しです。

これが特定の問題の特定に役立つかどうかをお知らせください。これをもう少しデバッグするために、ミドルウェアを共有していただければ幸いです。


あなたが正しいようです!私はそのミドルウェアrejection_for($e->getMessage())rejection_for($e)どこかでではなくを呼び出していました。私はデフォルトのミドルウェアの元のソース(ここではgithub.com/guzzle/guzzle/blob/master/src/Middleware.php#L106)を探していましたが、のrejection_for($e)代わりになぜ存在するのかまったくわかりませんでしたthrow $e。私のテストケースによると、それは同じようにカスケードするようです。簡略化されたテストケースについては、元の投稿を参照してください。
フレーム

1
@Flame私はあなたを助けることができてうれしい:)あなたの2番目の質問によれば:それらの間に違いがあるかどうか。まあそれは本当にユースケース次第です。特定のシナリオでは、呼び出しが1つしかないため、使用される例外クラスを除いて、違いはありません。一度に複数の非同期呼び出しに切り替えることを検討している場合は、他の要求がまだ実行されている間にコードの中断を回避するために、promiseの使用を検討する必要があります。私の回答を受け入れるために詳細情報が必要な場合は、お知らせください:)
クリストフクルージ

0

こんにちはあなたがあなたの問題を解決したかどうかわかりませんでした。

エラーログとは何かを投稿してください。PHPとサーバーのエラーログの両方で検索

私はあなたのフィードバックを待っています


1
例外は既に上に投稿されています。バックグラウンドプロセスとそれをスローするライン$client->request('GET', ...)(通常のguzzleクライアント)からのものであること以外に投稿するものはありません。

0

これはあなたの環境で散発的に起こり、RejectionException(少なくとも私はできませんでした)をスローして複製するのが難しいcatchので、コードに別のブロックを追加できますか?以下を参照してください:

try {
    $c = new \GuzzleHttp\Client([
        'timeout' => 0.1
    ]);
    $response = (string) $c->get('https://example.com')->getBody();
} catch (GuzzleHttp\Promise\RejectionException $e) {
    // Log the output of $e->getTraceAsString();
} 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.
}

それはあなたと私たちになぜ、いつこれが起こるのかについていくつかのアイデアを与えなければなりません。


悲しいことにそれはしません。スタックトレースを取得できなかったので、最終的にはLaravel例外ハンドラーに到達した(そしてSentryに送信された)ため、私はSentryでスタックトレースを取得しました。スタックトレースは、Guzzleライブラリの奥深くを指し示すだけですが、なぜそれが約束を前提としているのか理解できません。
フレーム

なぜそれが約束を仮定しているのかについての別の回答をご覧ください:stackoverflow.com/a/60498078/1568963
Vladimir
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.