У меня есть задания, которые выполняются на нескольких работниках очереди, которые содержат некоторые 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 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
. - Версия жрет
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
.
HandlerStack
)?Ответы:
Здравствуйте, я хотел бы знать, если у вас ошибка 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
Может быть, вы должны попробовать это
Жрать нужно кактус, если код ответа не 200
альтернатива 3
В моем случае это было потому, что я передал пустой массив в параметрах $ option ['json'], я не мог воспроизвести 500 на сервере, используя Postman или cURL, даже когда передавал заголовок запроса Content-Type: application / json.
В любом случае, удаление ключа json из массива параметров запроса решило проблему.
Я потратил около 30 минут, пытаясь понять, что не так, потому что это поведение очень противоречиво. Для всех других запросов, которые я делаю, передача $ options ['json'] = [] не вызывала проблем. Это может быть проблема с сервером, но я не контролирую сервер.
отправить отзыв о полученных данных
источник
ConnectException
нет связанного ответа, поэтому, насколько я знаю, нет ошибки 400 или 500. Похоже, что вы должны ловитьBadResponseException
(илиClientException
(4xx) /ServerException
(5xx), которые оба являются его детьми)Guzzle использует Promises для синхронных и асинхронных запросов. Разница лишь в том, что когда вы используете синхронный запрос (ваш случай) - он выполняется сразу же, вызывая
wait()
метод . Обратите внимание на эту часть:Таким образом, он выбрасывает,
RequestException
который является экземпляром,\Exception
и это всегда происходит при ошибках HTTP 4xx и 5xx, если исключение не исключено с помощью опций. Как вы видите, он также может выдать,RejectionException
если причина не является примером,\Exception
например, если причина - строка, которая, кажется, происходит в вашем случае. Странная вещь заключается в том, что вы получаете,RejectException
а неRequestException
как Guzzle бросаетConnectException
на ошибку времени ожидания подключения. В любом случае, вы можете найти причину, если просмотритеRejectException
трассировку стека в Sentry и узнаете, гдеreject()
метод вызывается в Promise.источник
Обсуждение с автором в разделе комментариев как начало моего ответа:
Вопрос:
Ответ автора:
В соответствии с этим вот мой тезис:
У вас есть тайм-аут в одном из ваших промежуточных программ, который вызывает жрет. Итак, давайте попробуем реализовать воспроизводимый случай.
Здесь у нас есть специальное промежуточное программное обеспечение, которое вызывает guzzle и возвращает ошибку отказа с сообщением об исключении из дополнительного вызова. Это довольно сложно, потому что из-за внутренней обработки ошибок он становится невидимым внутри трассировки стека.
Это тестовый пример, как вы можете использовать его:
Как только я выполню тест против этого, я получаю
Таким образом, похоже, что ваш основной вызов не прошел, но на самом деле это дополнительный вызов.
Дайте мне знать, если это поможет вам определить вашу конкретную проблему. Я также был бы очень признателен, если бы вы могли поделиться своим промежуточным программным обеспечением, чтобы отладить это немного дальше.
источник
rejection_for($e->getMessage())
неrejection_for($e)
где-то в этом промежуточном программном обеспечении. Я искал исходный источник для промежуточного ПО по умолчанию (например, здесь: github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106 ), но не мог сказать, почему они былиrejection_for($e)
вместоthrow $e
. Похоже, так же, как в моем тестовом примере. Смотрите оригинальный пост для упрощенного теста.Здравствуйте, я не понял, в конечном итоге вы решили свою проблему или нет.
Ну, я хотел бы, чтобы вы опубликовали, что такое журнал ошибок. Поиск как в PHP, так и в журнале ошибок вашего сервера
Я жду вашего отзыва
источник
$client->request('GET', ...)
(просто обычный клиент-жрет).Поскольку это происходит спорадически в вашей среде, и его трудно воспроизвести, бросая
RejectionException
(по крайней мере, я не мог), вы можете просто добавить еще одинcatch
блок в ваш код, см. Ниже:Это должно дать вам и нам несколько идей о том, почему и когда это происходит.
источник