Saya memiliki pekerjaan yang berjalan di beberapa pekerja antrian, yang berisi beberapa permintaan HTTP menggunakan Guzzle. Namun, blok coba-tangkap di dalam pekerjaan ini tampaknya tidak mengambil GuzzleHttp\Exception\RequestException
ketika saya menjalankan pekerjaan ini dalam proses latar belakang. Proses yang sedang berjalan adalah php artisan queue:work
yang merupakan pekerja sistem antrian Laravel yang memantau antrian dan mengambil pekerjaan.
Sebaliknya, pengecualian yang dilontarkan adalah salah satu dari GuzzleHttp\Promise\RejectionException
pesan tersebut:
Janji itu ditolak dengan alasan: kesalahan CURL 28: Operasi habis setelah 30001 milidetik dengan 0 byte diterima (lihat https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Ini sebenarnya disamarkan GuzzleHttp\Exception\ConnectException
(lihat https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 ), karena jika saya menjalankan pekerjaan serupa dalam proses PHP biasa yang dipicu dengan mengunjungi sebuah URL, saya mendapatkan yang ConnectException
dimaksud dengan pesan:
kesalahan CURL 28: Waktu operasi habis setelah 100 milidetik dengan 0 dari 0 byte diterima (lihat https://curl.haxx.se/libcurl/c/libcurl-errors.html )
Kode contoh yang akan memicu batas waktu ini:
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.
}
Kode di atas melempar baik RejectionException
atau ConnectException
ketika berlari dalam proses pekerja, tetapi selalu ConnectException
ketika diuji secara manual melalui browser (dari apa yang saya tahu).
Jadi pada dasarnya yang saya dapatkan, adalah bahwa ini RejectionException
adalah pembungkus pesan dari ConnectException
, namun saya tidak menggunakan fitur asinkron dari Guzzle. Permintaan saya hanya dilakukan secara seri. Satu-satunya hal yang berbeda adalah bahwa beberapa proses PHP mungkin membuat panggilan HTTP Guzzle atau bahwa pekerjaan itu sendiri adalah waktu habis (yang seharusnya menghasilkan pengecualian berbeda menjadi Laravel Illuminate\Queue\MaxAttemptsExceededException
), tetapi saya tidak melihat bagaimana ini menyebabkan kode berperilaku berbeda.
Saya tidak bisa menemukan kode apa pun di dalam paket Guzzle yang menggunakan php_sapi_name()
/ PHP_SAPI
(yang menentukan antarmuka yang digunakan) untuk mengeksekusi hal-hal yang berbeda ketika menjalankan dari CLI sebagai lawan dari pemicu browser.
tl; dr
Mengapa Guzzle melempar saya RejectionException
ke proses pekerja saya, tetapi ConnectException
pada skrip PHP biasa yang dipicu melalui browser?
Edit 1
Sedihnya saya tidak bisa membuat contoh minimal yang bisa direproduksi. Saya melihat banyak pesan kesalahan dalam pelacak masalah Sentry saya, dengan pengecualian tepat seperti yang ditunjukkan di atas. Sumber dinyatakan sebagai Starting Artisan command: horizon:work
(yaitu Laravel Horizon, mengawasi antrian Laravel). Saya telah memeriksa lagi untuk melihat apakah ada perbedaan antara versi PHP, tetapi baik situs web dan proses pekerja menjalankan PHP 7.3.14
yang sama yang benar:
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
- Versi CURL adalah
cURL 7.58.0
. - Versi guzzle adalah
guzzlehttp/guzzle 6.5.2
- Versi Laravel adalah
laravel/framework 6.12.0
Edit 2 (tumpukan jejak)
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
The Client::callRequest()
Fungsi berisi hanya membuang waktu Client yang saya sebut $client->request($request['method'], $request['url'], $request['options']);
(jadi im tidak menggunakan requestAsync()
). Saya pikir itu ada hubungannya dengan menjalankan pekerjaan secara paralel yang menyebabkan masalah ini.
Edit 3 (solusi ditemukan)
Pertimbangkan testcase berikut yang membuat permintaan HTTP (yang seharusnya mengembalikan 200 respons reguler):
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);
}
Sekarang apa yang saya lakukan awalnya adalah panggilan rejection_for($e->getMessage())
yang membuat sendiri RejectionException
berdasarkan pada string pesan. Memanggil rejection_for($e)
adalah solusi yang tepat di sini. Satu-satunya yang tersisa untuk dijawab adalah jika rejection_for
fungsi ini sama dengan yang sederhana throw $e
.
HandlerStack
?