Жрет бросая RejectionException вместо ConnectionException в фоновом процессе - PullRequest
9 голосов
/ 13 февраля 2020

У меня есть задания, которые выполняются на нескольких работниках очереди, которые содержат некоторые HTTP-запросы, использующие Guzzle. Тем не менее, блок try-catch внутри этого задания, похоже, не получает GuzzleHttp\Exception\RequestException, когда я запускаю это задание в фоновом процессе. Запущенный процесс - php artisan queue:work, который является Laravel системным работником очереди, который контролирует очередь и выбирает задания.

Вместо этого выбрасывается исключение GuzzleHttp\Promise\RejectionException с сообщением:

Обещание было отклонено по причине: ошибка cURL 28: Тайм-аут операции после 30001 миллисекунды с получением 0 байтов (см. 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: Тайм-аут операции по истечении 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 могут выполнять HTTP-вызовы Guzzle или что сами задания задерживаются (что должно привести к другому исключению, равному Laravel s Illuminate\Queue\MaxAttemptsExceededException), но я не понимаю, как это приводит к тому, что код ведет себя по-разному.

Я не смог найти какой-либо код в пакетах Guzzle, который использует php_sapi_name() / PHP_SAPI (который определяет используемый интерфейс) для выполнения других операций при запуске из CLI как в отличие от триггера браузера.

tl; dr

Почему Guzzle бросает мне RejectionException с на моих рабочих процессах, но ConnectException с на обычных PHP сценариях, запускаемых через браузер?

Редактировать 1

К сожалению, я не могу создать минимальный воспроизводимый пример. Я вижу много сообщений об ошибках в моем трекере ошибок Sentry, с точным исключением, показанным выше. Источник указан как Starting Artisan command: horizon:work (то есть Laravel Horizon, он контролирует очереди Laravel). Я еще раз проверил, есть ли расхождения между 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 Версия 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.

Ответы [ 5 ]

3 голосов
/ 27 февраля 2020

Здравствуйте, я хотел бы знать, если у вас ошибка 4xx или ошибка 5xx

Но даже в этом случае я добавлю несколько альтернатив для найденных решений, которые напоминают вашу проблему

вариант 1

Я хотел бы устранить это, у меня была эта проблема с новым рабочим сервером, возвращавшим неожиданные 400 ответов по сравнению со средой разработки и тестирования, работающей как ожидалось; простая установка apt install php7 .0-curl это исправило.

Это была новая установка Ubuntu 16.04 LTS с php, установленная через ppa: ondrej / php, во время отладки я заметил, что заголовки были разные. Оба отправляли форму, состоящую из нескольких частей, с проверенными данными, однако без php7 .0-curl она отправляла заголовок Connection: close, а не Expect: 100-Continue; оба запроса которых имели 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

альтернатива 3

В моем случае это произошло потому, что я передал пустой массив в параметрах $ options ['json'], которые я не мог не воспроизводите 500 на сервере с помощью Postman или cURL даже при передаче заголовка запроса Content-Type: application / json.

В любом случае удаление ключа json из массива параметров запроса решило проблему .

Я потратил около 30 минут, пытаясь понять, что не так, потому что это поведение очень противоречиво. Для всех других запросов, которые я делаю, передача $ options ['json'] = [] не вызывала проблем. Это может быть проблема с сервером, но я не управляю сервером.

отправить отзыв о полученных данных

2 голосов
/ 03 марта 2020

Guzzle использует Promises для синхронных и асинхронных запросов. Единственное отличие состоит в том, что когда вы используете синхронный запрос (ваш случай) - он сразу же выполняется путем вызова wait() метода . Обратите внимание на эту часть:

Вызов wait по обещанию, которое было отклонено, вызовет исключение. Если причиной отклонения является экземпляр \Exception, причина выбрасывается. В противном случае выдается GuzzleHttp\Promise\RejectionException, и причина может быть получена путем вызова метода getReason исключения.

Таким образом, он выдает RequestException, который является экземпляром \Exception, и это всегда происходит при ошибках HTTP 4xx и 5xx, если исключение не отключено с помощью опций. Как видите, он также может выдать RejectionException, если причина не является экземпляром \Exception, например, если причиной является строка, которая, кажется, происходит в вашем случае. Странно то, что вы получаете RejectException вместо RequestException, поскольку Guzzle выдает ConnectException в случае ошибки тайм-аута соединения. В любом случае, вы можете найти причину, если вы go через трассировку стека RejectException в Sentry и найдете, где метод reject() вызывается в Promise.

1 голос
/ 04 марта 2020

Обсуждение с автором в разделе комментариев как начало моего ответа:

Вопрос:

У вас есть пользовательское промежуточное ПО для жрета на месте ( подсказка: HandlerStack)?

Ответ автора:

Да, разные. Но промежуточное ПО - это, по сути, модификатор запроса / ответа, даже те жадные запросы, которые я там выполняю, выполняются синхронно.


В соответствии с этим вот мой тезис:

У вас есть тайм-аут внутри одного из промежуточных программ, который вызывает жрет. Итак, давайте попробуем реализовать воспроизводимый случай.

Здесь у нас есть специальное промежуточное ПО, которое вызывает 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)"

Так что похоже, что ваш основной вызов жрет неудачно, но на самом деле это дополнительный вызов, который не удался.

Дайте мне знать, если это поможет вам определить вашу конкретную c проблему. Я также был бы очень признателен, если бы вы могли поделиться своим промежуточным программным обеспечением, чтобы отладить это немного дальше.

0 голосов
/ 02 марта 2020

Поскольку это происходит спорадически в вашей среде, и трудно воспроизвести бросок 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.
}

Он должен дать вам и нам несколько идей о том, почему и когда это происходит.

0 голосов
/ 28 февраля 2020

Здравствуйте, я не понял, в конечном итоге вы решили свою проблему или нет.

Хорошо, я бы хотел, чтобы вы опубликовали, что такое журнал ошибок. Поиск как в PHP, так и в журнале ошибок вашего сервера

Жду ваших отзывов

...