当前位置: 首页 > 知识库问答 >
问题:

Guzzle在后台进程中抛出RejectionException而不是ConnectionException

马奇略
2023-03-14

我有在多个队列工作器上运行的作业,其中包含一些使用Guzzle的HTTP请求。但是,当我在后台进程中运行这些作业时,此作业中的try-catch块似乎不会拾取GuzzleHttp\Exception\Request estException。正在运行的进程是一个php artisan队列:work,它是一个Laravel队列系统工作器,用于监控队列并拾取作业。

相反,抛出的异常是带有以下消息的GuzzleHttp\Promise\RejtionException之一:

promise被拒绝,原因如下: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),因为如果我在由访问URL触发的常规PHP进程中运行类似的作业,我确实会得到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.
}

当在工作进程中运行时,上面的代码会抛出拒绝异常ConnectException,但在通过浏览器手动测试时总是抛出ConnectException(据我所知)。

因此,基本上我得出的结论是,这个RejectiveException包装了来自ConnectException的消息,但是我没有使用Guzzle的异步功能。我的请求只是串联完成的。唯一不同的是,多个PHP进程可能正在进行Guzzle HTTP调用,或者作业本身正在超时(这应该会导致一个不同的例外,即Laravel的Illuminate\Queue\MaxAttemptsExceedException),但我不明白这如何导致代码的行为不同。

在Guzzle软件包中,我找不到任何代码使用<code>php_sapi_name()/<code>php\u sapi(确定使用的接口)在从CLI运行时执行不同的东西,而不是浏览器触发器。

为什么Guzzle会在我的工作进程上抛出RejectionException,而在通过浏览器触发的常规PHP脚本上抛出ConnectException?

遗憾的是,我无法创建一个最小的可重复示例。我在我的哨兵问题跟踪器中看到了许多错误消息,上面显示了一个例外。源代码称为启动Artisan命令:horizon:work(这是Laravel horizon,它监督Laravel队列)。我再次检查了PHP版本之间是否存在差异,但网站和工作进程都运行相同的PHP7.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版本是http/Guzzle 6.5.2
  • Laravel版本是<code>Laravel/framework 6.12.0</code>
    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-

考虑下面的testcase,它发出一个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-


共有3个答案

呼延烈
2023-03-14

在评论区与作者讨论,作为我回答的开始:

问题:

您是否有定制的guzzle中间件(提示:HandlerStack)?

作者的回答:

是的,各种各样。但中间件基本上是一个请求/响应修改器,即使我在那里发出的大量请求也是同步完成的。

根据这个,这是我的论文:

您的一个中间件中有一个超时,称为guzzle。所以,让我们尝试实现一个可重复的案例。

这里我们有一个自定义中间件,它调用guzzle并返回一个拒绝失败以及子调用的异常消息。这很棘手,因为由于内部的错误处理,它在stack-trace中是不可见的。

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)"

所以看起来你的主狂饮呼叫失败了,但实际上是子呼叫失败了。

如果这有助于您确定您的具体问题,请告诉我。如果你能分享你的中间件来进一步调试,我将非常感激。

柯书
2023-03-14

Guzz对同步和异步请求都使用Promises。唯一的区别是,当您使用同步请求(您的情况)时-它会通过调用etc()方法立即实现。注意这部分:

对已拒绝的promise调用等待将引发html" target="_blank">异常。如果拒绝原因是 \Exception 的实例,则引发原因。否则,将引发 GuzzleHttp\Promise\RejectException,并且可以通过调用异常的 getReason 方法获取原因。

因此,它抛出 RequestException,这是 \Exception 的一个实例,它总是发生在 4xx 和 5xx HTTP 错误上,除非通过选项禁用引发异常。如您所见,如果原因不是 \Exception 的实例,例如,如果原因是一个字符串,在您的案例中似乎发生,它也可能引发拒绝异常。奇怪的是,你得到的是RejectException而不是RequestException,因为Guzzle在连接超时错误时会抛出ConnectException。无论如何,如果你在 Sentry 中遍历了 RejectException 堆栈跟踪,并找到在 Promise 上调用 reject() 方法的位置,你可能会找到一个原因。

宫俊远
2023-03-14

您好,我想知道您是否有错误4xx或错误5xx

但即便如此,我还是会为找到的类似你的问题的解决方案提供一些替代方案

备选方案1

我想取消这个,我有这个问题,一个新的生产服务器返回了意外的400个响应,与开发和测试环境的预期工作相比;简单安装apt安装php7.0-curl就搞定了。

这是一个全新的Ubuntu 16.04 LTS安装,通过ppa: ondrej/php安装了php,在调试过程中我注意到标头不同。两者都在发送一个包含chucked数据的多部分表单,但是如果没有php7.0-curl,它会发送一个Connection:关闭标头,而不是预期:100-继续;这两个请求都有Transfer-Encode: chunked。

也许你应该试试这个

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需要cacching

备选案文3

在我的例子中,因为我在请求的$options['json']中传递了一个空数组,所以即使传递内容类型:application/json请求头,我也无法使用Postman或cURL在服务器上复制500。

无论如何,从请求的选项数组中删除json键解决了这个问题。

我花了30分钟试图找出问题所在,因为这种行为非常不一致。对于我提出的所有其他请求,传递$options['json']=[]不会引起任何问题。这可能是服务器问题,因为我无法控制服务器。

发送关于获得的详细信息的反馈

 类似资料:
  • 我在服务层的spring-boot应用程序中使用了Hystrix(Camden.sr7版本),而没有回退方法。Service的方法之一如下所示: 对于这样的响应,不清楚实际上是从哪个方法抛出异常的。如果我将版本更改为brixton.sr5(以前的版本),它将返回清晰的响应: 因此Hystrix的新版本(实际上是spring-cloud-dependencies的新版本)不会抛出HystrixRun

  • 既然我们可以在Javascript中使用关键字抛出任何东西,那么我们就不能直接抛出一个错误消息字符串吗? 有人知道这里面有什么陷阱吗? 让我对此添加一些背景:在JavaScript世界中,人们通常依赖参数检查而不是使用try-catch机制,因此只使用抛出致命错误是有意义的。不过,为了能够捕捉一些系统错误,我必须为我自己的错误使用一个不同的类,而不是创建错误的子类,我认为我应该只使用String。

  • 问题内容: 我的目标很简单:启动rsync并不要等待。 Debian上的Python 2.7.9 样例代码: (我之所以注释掉执行命令,只是因为我实际上将所有试验都保留在代码中,以便我知道自己已经做过什么以及尚未完成什么。显然,我会以正确的方式运行脚本行未注释。) 这是怎么回事…我可以在服务器上观看传输,完成传输后,我会在屏幕上看到“完成”字样。 我想发生的是在发出命令并立即开始传输后立即打印“完

  • 问题内容: 我正在尝试制作一个PHP脚本,我已经完成了脚本,但是大约要花10分钟才能完成它打算完成的过程。这不是问题,但是我想我必须一直保持页面加载,这很烦人。我可以使用它来启动该过程,然后在10分钟后再回来查看它生成的日志文件吗? 问题答案: 好吧,您可以使用“ ignore_user_abort(true)” 因此,脚本将继续起作用(注意脚本的持续时间,也许添加“set_time_limit(

  • 问题内容: 我有一个需要使用Jackson从JSON反序列化的类。类结构如下所示: 反序列化对象通常效果很好;除了,它与错误代码互操作,当列表为空时,错误代码发出错误的值。也就是说,不是发出: 它发出: 杰克逊遇到以下情况时会抛出此异常: 当然,所有这些都是有道理的。输入错误。 但是,这种情况下的空列表与任何代码都不相关;如果是的话,null我不在乎。null如果无法反序列化该属性,是否有任何方法