Guzzle кидає RejectionException замість ConnectionException у фоновому процесі


9

У мене є завдання, які працюють на кількох чергових працівників, які містять деякі HTTP-запити за допомогою Guzzle. Однак блок пробного захоплення всередині цієї роботи, схоже, не з’являється, 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.
}

Код, викладений вище, містить або a, RejectionExceptionабо ConnectExceptionзапускається в робочому процесі, але завжди a, ConnectExceptionколи тестується вручну через браузер (з того, що я можу сказати).

Тому в основному те, що я отримую, це те, що це RejectionExceptionзавершення повідомлення від ConnectException, однак я не використовую асинхронні функції Guzzle. Мої запити виконуються просто послідовно. Єдине, що відрізняється, це те, що кілька процесів PHP можуть здійснювати HTZ-дзвінки Guzzle або що самі завдання закінчуються тимчасово (що може призвести до іншого винятку, як Laravel Illuminate\Queue\MaxAttemptsExceededException), але я не бачу, як це спричиняє поведінку коду по-різному.

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

тл; д-р

Чому 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()Функція містить тільки клієнт жерти , на якому я називаю $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? Скільки працівників працює паралельно за інстанцією / екземпляром? Чи є на місці спеціальні програмні засоби для загадок (підказка:) HandlerStack?
Крістоф Клюге

Чи можете ви надати слід стека від Sentry?
Володимир

@Vladimir ive додав слід стека. Я не думаю, що це дуже допоможе тобі. Те, як обіцянки реалізуються в Guzzle (і PHP взагалі), важко читати.
Полум’я

1
@Flame, чи можете ви поділитися середнім програмним забезпеченням, яке виконує запит під заголовком? Я думаю, питання буде там. Тим часом я додам повторювану відповідь зі своєю тезою.
Крістоф Клюге

Відповіді:


3

Привіт, я хотів би дізнатися, чи є у вас помилка 4xx або помилка 5xx

Але навіть я поставлю кілька альтернатив для знайдених рішень, що нагадують вашу проблему

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

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

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

Guzzle потребує кактування, якщо код відповіді не 200

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

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

У будь-якому разі видалення ключа json з масиву параметрів запиту вирішило проблему.

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

надсилати відгуки про отримані деталі


добре ... Щоб мати більш швидку і точну відповідь. Я взяв на себе ініціативу розмістити це питання на сторінці проекту на GitHub. Сподіваюся, ви не заперечуєте проти github.com/guzzle/guzzle/isissue/2599
PauloBoaventura

1
a ConnectExceptionне має відповідної відповіді, тому, наскільки мені відомо, немає помилки 400 або 500. Схоже, ви насправді ловите BadResponseException(або ClientException(4xx) / ServerException(5xx), які є обома дітьми)
Полум'я


2

Guzzle використовує Promises як для синхронних, так і для асинхронних запитів. Єдина відмінність полягає в тому, що при використанні синхронного запиту (ваш випадок) - він виконується відразу за допомогою виклику wait() методу . Зверніть увагу на цю частину:

Заклик wait, який було відхилено, призведе до виключення. Якщо причиною відхилення є примірник \Exceptionпричини, викидається. В іншому випадку a GuzzleHttp\Promise\RejectionException кидається, і причину можна отримати, викликавши getReason метод винятку.

Отже, він викидає, RequestExceptionщо є екземпляром, \Exceptionі це завжди трапляється на помилках HTTP 4xx та 5xx, якщо викидання виключень не вимкнено через параметри. Як бачите, він може також кинути, RejectionExceptionякщо причина не є примірником, \Exceptionнаприклад, якщо причина - це рядок, який, здається, трапляється у вашому випадку. Дивна річ у тому, що ви отримуєте RejectExceptionзамість того, RequestExceptionяк Guzzle викидає ConnectExceptionпомилку очікування з'єднання. У будь-якому випадку, ви можете знайти причину, якщо пройти ваш RejectExceptionстек слід у Sentry і знайти, де reject()метод викликається Promise.


1

Обговорення з автором у розділі коментарів як початковий текст моєї відповіді:

Питання:

Чи є на місці спеціальні програмні засоби для ґудзиків (підказка: 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)"

Таким чином, схоже, що ваш основний дзвінок із загадки не вдався, але насправді це підзаклик, який не вдався.

Повідомте мене, чи допоможе вам визначити вашу конкретну проблему. Я також дуже вдячний, якщо ви можете поділитися своїм середнім рівнем, щоб налагодити це трохи далі.


Схоже, ти маєш рацію! Я закликав rejection_for($e->getMessage())замість того, щоб rejection_for($e)десь у цьому проміжному програмному забезпеченні. Я дивився на початкове джерело проміжного програмного забезпечення за замовчуванням (наприклад, тут: github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106 ), але не міг точно сказати, чому там rejection_for($e)замість цього throw $e. Схоже, це каскад так само, як і моя тестова шафа. Див. Оригінальну публікацію для спрощеного тесту.
Полум’я

1
@Flame рада, що можу вам допомогти :) Відповідно до вашого другого питання: Якщо між ними є різниця. Ну, це справді залежить від використання. У вашому конкретному сценарії це не вплине (крім використовуваного класу виключень), оскільки у вас є лише поодинокі дзвінки. Якщо ви вирішили перейти на багаторазові та асинхронізовані дзвінки одразу, вам варто розглянути можливість обіцянки уникнути переривання коду, поки інші запити все ще запущені. Якщо вам потрібна додаткова інформація, щоб прийняти мою відповідь, повідомте мене про це :)
Крістоф Клюге

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 (і надсилається до Sentry). Слід стека лише вказує на мене глибоко в бібліотеці Guzzle, але я не можу зрозуміти, чому він бере на себе обіцянку.
Полум’я

Дивіться ще одну мою відповідь щодо того, чому вона бере на себе обіцянку: stackoverflow.com/a/60498078/1568963
Володимир
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.