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
?Jawaban:
Halo Saya ingin tahu apakah Anda mengalami kesalahan 4xx atau kesalahan 5xx
Namun meski begitu saya akan menempatkan beberapa alternatif untuk solusi yang menyerupai masalah Anda
alternatif 1
Saya ingin menabrak ini, saya memiliki masalah ini dengan server produksi baru yang mengembalikan 400 tanggapan yang tidak terduga dibandingkan dengan pengembangan dan lingkungan pengujian yang bekerja seperti yang diharapkan; cukup menginstal apt install php7.0-curl memperbaikinya.
Itu adalah Ubuntu baru 16,04 LTS instal dengan php diinstal melalui ppa: ondrej / php, selama debugging saya perhatikan bahwa header berbeda. Keduanya mengirim formulir multi-bagian dengan data yang dibuang, namun tanpa php7.0-curl ia mengirim Koneksi: tutup header daripada Harapan: 100-Lanjutkan; kedua permintaan yang memiliki Transfer-Pengkodean: chunked.
alternatif 2
Mungkin Anda harus mencoba ini
Guzzle perlu cactching jika kode respons tidak 200
alternatif 3
Dalam kasus saya itu karena saya telah melewati array kosong di $ options permintaan ['json'] saya tidak bisa mereproduksi 500 di server menggunakan Postman atau cURL bahkan ketika melewati Content-Type: application / json request header.
Bagaimanapun, menghapus kunci json dari array opsi permintaan memecahkan masalah.
Saya menghabiskan waktu 30 menit untuk mencari tahu apa yang salah karena perilaku ini sangat tidak konsisten. Untuk semua permintaan lain yang saya buat, meneruskan $ options ['json'] = [] tidak menyebabkan masalah. Ini bisa jadi masalah server, saya tidak mengontrol server.
kirim umpan balik tentang perincian yang diperoleh
sumber
ConnectException
tidak memiliki respons terkait, jadi oleh karena itu tidak ada kesalahan 400 atau 500 sejauh yang saya ketahui. Sepertinya Anda seharusnya menangkapBadResponseException
(atauClientException
(4xx) /ServerException
(5xx) yang sama-sama anak-anak itu)Guzzle menggunakan Janji untuk permintaan sinkron dan asinkron. Satu-satunya perbedaan adalah bahwa ketika Anda menggunakan permintaan sinkron (kasing Anda) - itu dipenuhi segera dengan memanggil
wait()
metode . Perhatikan bagian ini:Jadi, ia melempar
RequestException
yang merupakan contoh\Exception
dan selalu terjadi pada kesalahan HTTP 4xx dan 5xx, kecuali melempar pengecualian dinonaktifkan melalui opsi. Seperti yang Anda lihat, itu juga bisa melemparRejectionException
jika alasannya bukan merupakan contoh\Exception
misalnya jika alasannya adalah string yang tampaknya terjadi dalam kasus Anda. Yang aneh adalah bahwa Anda mendapatkanRejectException
daripadaRequestException
Guzzle melemparConnectException
kesalahan koneksi habis. Ngomong-ngomong, Anda dapat menemukan alasan jika AndaRejectException
menelusuri jejak tumpukan di Sentry dan menemukan di manareject()
metode ini dipanggil pada Janji.sumber
Diskusi dengan penulis di dalam bagian komentar sebagai starter untuk jawaban saya:
Pertanyaan:
Jawaban penulis:
Menurut ini di sini adalah tesis saya:
Anda memiliki batas waktu di dalam salah satu middleware Anda yang memanggilnya membuang waktu. Jadi mari kita coba menerapkan kasing yang dapat direproduksi.
Di sini kita memiliki middleware khusus yang memanggil guzzle dan mengembalikan kegagalan penolakan dengan pesan pengecualian dari sub-panggilan. Ini cukup sulit, karena karena penanganan kesalahan internal itu tidak terlihat di dalam stack-trace.
Ini adalah contoh uji bagaimana Anda dapat menggunakannya:
Segera setelah saya melakukan tes terhadap ini saya menerima
Jadi sepertinya panggilan guzzle utama Anda gagal tetapi pada kenyataannya itu adalah sub-panggilan yang gagal.
Beri tahu saya jika ini membantu Anda mengidentifikasi masalah spesifik Anda. Saya juga akan sangat menghargai jika Anda dapat membagikan middlewares Anda untuk men-debug ini sedikit lebih jauh.
sumber
rejection_for($e->getMessage())
bukanrejection_for($e)
di suatu tempat di middleware itu. Saya sedang mencari sumber asli untuk middleware default (seperti di sini: github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106 ), tetapi tidak bisa mengetahui mengapa adarejection_for($e)
alih - alihthrow $e
. Tampaknya mengalir dengan cara yang sama menurut testcase saya. Lihat posting asli untuk testcase yang disederhanakan.Halo, saya tidak mengerti apakah Anda akhirnya memecahkan masalah Anda atau tidak.
Yah saya ingin Anda memposting apa itu log kesalahan. Cari baik dalam PHP dan di dalam log kesalahan server Anda
Saya menunggu tanggapan Anda
sumber
$client->request('GET', ...)
(hanya klien membuang waktu biasa).Karena ini terjadi secara sporadis pada lingkungan Anda dan sulit untuk mereplikasi melempar
RejectionException
(setidaknya saya tidak bisa), dapatkah Anda menambahkancatch
blok lain ke kode Anda, lihat di bawah:Itu harus memberi Anda dan kami beberapa ide tentang mengapa dan kapan ini terjadi.
sumber