백그라운드 프로세스에서 ConnectionException 대신 RejectionException을 발생시키는 Guzzle


9

여러 큐 작업자에서 실행되며 Guzzle을 사용하는 일부 HTTP 요청이 포함 된 작업이 있습니다. 그러나 GuzzleHttp\Exception\RequestException백그라운드 작업에서 이러한 작업을 실행할 때이 작업 내의 try-catch 블록이 나타나지 않는 것 같습니다 . 실행중인 프로세스는 php artisan queue:work큐를 모니터하고 작업을 선택하는 Laravel 큐 시스템 작업자입니다.

대신, 예외는 GuzzleHttp\Promise\RejectionException다음 메시지 중 하나입니다 .

cURL 오류 28 : 0 바이트가 수신 된 30001 밀리 초 후에 작업 시간이 초과되었습니다 ( https://curl.haxx.se/libcurl/c/libcurl-errors.html 참조 ).

이것은 실제로 위장되어 있습니다 GuzzleHttp\Exception\ConnectException( https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 참조 ). 정규적인 PHP 프로세스에서 비슷한 작업을 실행하면 URL, ConnectException메시지에 의도 한대로 얻습니다 .

cURL 오류 28 : 0 바이트 중 0 개를 수신하여 100 밀리 초 후에 작업 시간이 초과되었습니다 ( 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 Horizon)이며 라 라벨 큐를 감독합니다. PHP 버전간에 불일치가 있는지 다시 확인했지만 웹 사이트와 작업자 프로세스 모두 동일한 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입니다.
  • Guzzle 버전은 guzzlehttp/guzzle 6.5.2
  • 라 라벨 버전은 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()기능에는 내가 호출하는 Guzzle Client가 포함되어 있습니다 $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
라 라벨에 어떤 큐 드라이버를 사용하십니까? 인스턴스 / 인스턴스에서 몇 명의 작업자가 동시에 실행되고 있습니까? 맞춤식 미들웨어가 준비되어 HandlerStack있습니까 (힌트 :) ?
Christoph Kluge

Sentry에서 스택 추적을 제공 할 수 있습니까?
블라디미르

@Vladimir ive는 스택 추적을 추가했습니다. 나는 그것이 당신에게 많은 도움이 될 것이라고 생각하지 않습니다. Guzzle (및 일반적으로 PHP)에서 약속이 구현되는 방식은 읽기 어렵다.
화염

1
@Flame 하위 퍼즐 요청을 수행하는 미들웨어를 공유 할 수 있습니까? 문제가있을 것 같아요. 그동안 논문과 함께 재현 가능한 답변을 추가하겠습니다.
Christoph Kluge

답변:


3

안녕하세요, 오류 4xx 또는 오류 5xx가 발생했는지 알고 싶습니다.

그러나 그럼에도 불구하고 문제와 비슷한 해결책을 제시 할 것입니다.

대안 1

이 문제를 해결하고 싶은데, 새로운 프로덕션 서버가 예상대로 작동하는 개발 및 테스트 환경에 비해 예기치 않은 400 응답을 반환하는이 문제가있었습니다. 단순히 apt install php7.0-curl을 설치하면 해결되었습니다.

ppa : ondrej / php를 통해 PHP를 설치 한 새로운 Ubuntu 16.04 LTS 설치였습니다. 디버깅하는 동안 헤더가 다르다는 것을 알았습니다. 둘 다 척킹 된 데이터로 여러 부분으로 된 양식을 전송했지만 php7.0-curl이 없으면 Expect : 100-Continue 대신 Connection : close header를 전송했습니다. 두 요청 모두 Transfer-Encoding : chunked.

  대안 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이 아닌 경우 Guzzle에 충돌이 필요

대안 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은 동기식 요청과 비동기식 요청 모두에 약속을 사용합니다. 유일한 차이점은 동기 요청 (귀하의 경우)을 사용할 때 wait() 메소드 를 호출하여 즉시 충족된다는 것입니다 . 이 부분을 참고하십시오 :

호출 wait예외가 발생합니다 거부 된 약속. 거부 사유가 \Exception해당 사유 의 인스턴스 인 경우 발생합니다. 그렇지 않으면, a GuzzleHttp\Promise\RejectionException 가 발생하고 getReason 예외 의 메소드를 호출하여 이유를 얻을 수 있습니다 .

따라서 예외를 던지는 것이 옵션을 통해 비활성화되지 않는 한 RequestException인스턴스 인 인스턴스를 throw \Exception하고 항상 4xx 및 5xx HTTP 오류에서 발생합니다. 보시다시피 RejectionException, 이유가 인스턴스가 아닌 경우 ( \Exception예 : 이유가 문자열 인 경우 문자열이 있는 경우)를 던질 수 있습니다 . 이상한 점은 Guzzle이 연결 시간 초과 오류를 발생 시키는 RejectException것이 아니라 얻는 것 입니다. 어쨌든 Sentry에서 스택 추적을 수행하고 Promise 에서 메소드가 호출 되는 위치를 찾으면 이유를 찾을 수 있습니다 .RequestExceptionConnectExceptionRejectExceptionreject()


1

내 답변의 출발점으로 댓글 섹션 내에서 저자와 토론 :

질문:

사용자 지정 guzzle 미들웨어가 준비되어 있습니까 (힌트 : HandlerStack)?

저자의 답변 :

예, 다양합니다. 그러나 미들웨어는 기본적으로 요청 / 응답 수정 자이며, 거기에서 내가 만든 총구 요청조차도 동 기적으로 수행됩니다.


이것에 따르면 여기 내 논문이 있습니다 :

미들웨어 중 하나에서 시간을내어 guzzle이라고 부릅니다. 재현 가능한 사례를 구현해 봅시다.

여기 우리는 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)"

그래서 그것은 주된 주둥이 호출이 실패한 것처럼 보이지만 실제로는 실패한 하위 호출입니다.

이것이 특정 문제를 식별하는 데 도움이되는지 알려주십시오. 또한 약간 더 디버깅하기 위해 미들웨어를 공유 할 수 있다면 대단히 감사하겠습니다.


당신이 옳은 것 같습니다! 나는 그 미들웨어 어딘가에 rejection_for($e->getMessage())대신 전화했다 rejection_for($e). 기본 미들웨어 ( github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106 ) 의 원래 소스를보고 있었지만 rejection_for($e)대신 왜 그 이유가 있는지 알 수 없었습니다 throw $e. 내 테스트 케이스에 따라 동일한 방식으로 계단식으로 보이는 것 같습니다. 단순화 된 테스트 사례는 원본 게시물을 참조하십시오.
화염

1
@ 불꽃 나는 당신을 도울 수있어서 기쁘다 :) 두 번째 질문에 따르면 : 그들 사이에 차이가 있다면. 글쎄, 그것은 실제로 유스 케이스에 달려 있습니다. 특정 시나리오에서는 단일 호출 만 있기 때문에 아무런 차이가 없습니다 (사용 된 예외 클래스 제외). 한 번에 여러 개의 비동기 호출로 전환하려면 다른 요청이 계속 실행되는 동안 코드 중단을 피하기위한 약속을 사용해야합니다. 내 대답을 받아들이 기 위해 더 많은 정보가 필요하면 알려주십시오. :)
Christoph Kluge

0

안녕하세요, 당신이 당신의 문제를 해결했는지 여부를 이해하지 못했습니다.

오류 로그가 무엇인지 게시하고 싶습니다. PHP와 서버의 오류 로그에서 모두 검색

나는 당신의 의견을 기다립니다


1
예외는 이미 위에 게시되어 있으며 백그라운드 프로세스에서 오는 것 이상을 게시하는 것 이상을 게시하는 것 이상은 없습니다 $client->request('GET', ...)(일반적인 주둥이 클라이언트).
화염

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

왜 그리고 언제 이런 일이 발생했는지에 대한 아이디어를 제공해야합니다.


슬프게도 그렇지 않습니다. Sentry에서 스택 추적을 얻었 기 때문에 결국 Laravel Exception 처리기에 도달하고 Sentry로 전송됩니다. 스택 추적은 Guzzle 라이브러리 내부에서만 나를 지적하지만 왜 약속을 가정하는지 알 수 없습니다.
화염

왜 약속을 가정하는지에 대한 또 다른 대답을보십시오 : stackoverflow.com/a/60498078/1568963
Vladimir
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.