Guzzle arkaplan işleminde ConnectionException yerine RejectionException özel durumlarını attı


9

Guzzle kullanarak bazı HTTP istekleri içeren birden çok kuyruk çalışanı üzerinde çalışan işlerim var. Ancak, GuzzleHttp\Exception\RequestExceptionbu işi arka plan işleminde çalıştırdığımda bu iş içindeki try-catch bloğu görünmüyor . Çalışan işlem, php artisan queue:workkuyruğu izleyen ve işleri alan bir Laravel kuyruk sistemi çalışanıdır.

Bunun yerine, atılan istisna şu GuzzleHttp\Promise\RejectionExceptioniletiden biridir:

Vaat nedeni ile reddedildi: cURL hatası 28: İşlem 0 bayt ile 30001 milisaniyeden sonra zaman aşımına uğradı (bkz. Https://curl.haxx.se/libcurl/c/libcurl-errors.html )

Bu aslında gizlenmiş GuzzleHttp\Exception\ConnectException(bkz. Https://github.com/guzzle/promises/blob/master/src/RejectionException.php#L22 ), çünkü benzer bir işi ziyaret ederek tetiklenen düzenli bir PHP işleminde çalıştırırsam URL, ben ConnectExceptionmesaj ile amaçlandığı gibi olsun :

cURL hatası 28: 0 baytın 0'ının 0'ı alınarak 100 milisaniyeden sonra zaman aşımına uğradı (bkz. https://curl.haxx.se/libcurl/c/libcurl-errors.html )

Bu zaman aşımını tetikleyecek örnek kod:

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.
}

Yukarıdaki kod ya çalışan işlemde RejectionExceptionya ConnectExceptionda çalıştırıldığında atar , ancak her ConnectExceptionzaman tarayıcı aracılığıyla manuel olarak test edildiğinde (anlatabileceğimden).

Temelde elde ettiğim şey, bunun RejectionExceptionmesajı kaydırmasıdır ConnectException, ancak Guzzle'ın asenkron özelliklerini kullanmıyorum. İsteklerim basitçe seri olarak yapılır. Farklı olan tek şey birden fazla PHP işleminin Guzzle HTTP çağrıları yapması veya işlerin kendisinin zaman aşımına uğraması (ki bu da Laravel'in farklı bir istisnasına neden olması Illuminate\Queue\MaxAttemptsExceededException), ancak bunun kodun farklı davranmasına neden olduğunu görmüyorum.

CLI'den bir tarayıcı tetikleyicisinin aksine çalıştırırken farklı şeyler yürütmek için kullanılan php_sapi_name()/ PHP_SAPI(kullanılan arabirimi belirler) Guzzle paketleri içinde herhangi bir kod bulamadım .

tl; Dr.

Neden Guzzle beni alt RejectionExceptionsüreçlerime atıyor , ancak ConnectExceptiontarayıcı aracılığıyla tetiklenen düzenli PHP komut dosyalarına atıyor ?

Düzenle 1

Ne yazık ki minimal tekrarlanabilir bir örnek oluşturamıyorum. Yukarıda belirtilen istisna dışında, Sentry sorun izleyicimde birçok hata mesajı görüyorum. Kaynak olarak belirtilir Starting Artisan command: horizon:work(Laravel Horizon, Laravel kuyruklarını denetler). PHP sürümleri arasında bir tutarsızlık olup olmadığını görmek için tekrar kontrol ettim, ancak hem web sitesi hem de çalışan işlemler aynı PHP'yi çalıştırıyor 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 sürümü cURL 7.58.0.
  • Guzzle sürümü guzzlehttp/guzzle 6.5.2
  • Laravel sürümü laravel/framework 6.12.0

Düzenleme 2 (yığın izlemesi)

    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

Bu Client::callRequest()fonksiyon sadece çağırdığım bir Guzzle İstemcisi içerir $client->request($request['method'], $request['url'], $request['options']);(bu yüzden im kullanmıyorum requestAsync()). Bu soruna neden olan işleri paralel olarak yürütmekle ilgili bir şey olduğunu düşünüyorum.

Edit 3 (çözüm bulundu)

Bir HTTP isteği yapan (normal 200 yanıt döndürmesi gereken) aşağıdaki test durumunu düşünün:

        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);
        }

Şimdi aslında yaptığım mesaj dizesine göre rejection_for($e->getMessage())kendi oluşturur çağrı oldu RejectionException. Arama rejection_for($e)burada doğru çözümdü. Yanıtlanması gereken tek şey, bu rejection_forfonksiyon bir basit ile aynı ise throw $e.


Hangi Guzzle sürümünü kullanıyorsunuz?
Vladimir

1
Laravel için hangi kuyruk sürücüsünü kullanıyorsunuz? Örnek / örnek başına kaç işçi paralel olarak çalışıyor? Özel guzzle ara katman yazılımlarınız var HandlerStackmı (ipucu:) ?
Christoph Kluge

Sentry'den yığın izlemesi sağlayabilir misiniz?
Vladimir

@ Vladimir ive yığın izini ekledi. Sana çok yardımcı olacağını sanmıyorum. Guzzle'da (ve genel olarak PHP'de) vaatlerin nasıl uygulandığını okumak zor.
Alev

1
@ Alev, alt bağlantı isteğini gerçekleştiren ara katman yazılımını paylaşabilir misiniz? Sorun orada olacak sanırım. Bu arada tezime tekrarlanabilir bir cevap ekleyeceğim.
Christoph Kluge

Yanıtlar:


3

Merhaba Hata 4xx veya hata 5xx olup olmadığını bilmek istiyorum

Ama buna rağmen, probleminize benzeyen çözümler için bazı alternatifler koyacağım

alternatif 1

Bunu çarpıştırmak istiyorum, beklediğim gibi çalışan geliştirme ve test ortamına kıyasla beklenmedik 400 yanıt döndüren yeni bir üretim sunucusunda bu sorunu yaşadım; basitçe yüklemek apt install php7.0-curl düzeltti.

Phpa ppa: ondrej / php üzerinden yüklenen yepyeni bir Ubuntu 16.04 LTS kurulumuydu, hata ayıklama sırasında başlıkların farklı olduğunu fark ettim. Her ikisi de chucked verileri olan çok parçalı bir form gönderiyorlardı, ancak php7.0-curl olmadan bir Bağlantı gönderiyordu: Bekleme: 100-Devam; her iki isteği de Transfer Kodlaması olan: yığınlanmış.

  alternatif 2

Belki bunu denemelisin

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());

Yanıt kodu 200 değilse Guzzle'ın kırılması gerekir

alternatif 3

Benim durumumda isteğin $ seçenekleri ['json'] boş bir dizi geçmişti çünkü Content-Type: application / json istek üstbilgisini geçerken bile Postman veya cURL kullanarak sunucuda 500 çoğaltmak olamazdı.

Her neyse, isteğin seçenekler dizisinden json anahtarını kaldırmak sorunu çözdü.

Neyin yanlış olduğunu anlamaya çalışmak için 30 dakika harcadım çünkü bu davranış çok tutarsız. Yaptığım diğer tüm istekler için $ options ['json'] = [] iletmek sorun yaratmadı. Bir sunucu sorunu olabilir tho, ben sunucuyu kontrol etmiyorum.

elde edilen detaylar hakkında geri bildirim gönderin


iyi ... Daha hızlı ve daha doğru bir cevap almak için. Soruyu GitHub'daki Proje Sayfasına gönderme girişimini yaptım. Umarım umursamıyorsun github.com/guzzle/guzzle/issues/2599
PauloBoaventura

1
Bir ConnectExceptionilişkili bir yanıtı yoktur, bu nedenle ben farkındayım kadarıyla hiçbir 400 veya 500 hata var. Aslında her ikisinin de çocukları olan BadResponseException(veya ClientException(4xx) / ServerException(5xx) 'i yakalamalısınız gibi görünüyor
Alev


2

Guzzle Promises'ı hem senkronize hem de senkronize olmayan istekler için kullanır. Tek fark, senkronize talebi (durumunuz) kullandığınızda - bir wait() yöntem çağrılarak hemen yerine getirilmesi . Bu bölüme dikkat edin:

waitReddedilen bir sözü çağırmak bir istisna atar. Eğer reddetme sebebi ise sebebin bir örneği \Exceptionatılır. Aksi takdirde, a GuzzleHttp\Promise\RejectionException atılır ve getReason istisna yöntemi çağrılarak neden elde edilebilir .

Yani, RequestExceptionbir örnek olan atar \Exceptionve istisnalar seçenekler aracılığıyla devre dışı bırakılmadığı sürece her zaman 4xx ve 5xx HTTP hatalarında olur. Gördüğünüz gibi, bunun RejectionExceptionnedeni \Exceptionörneğin bir örneği değilse, örneğin durumunuzda görünen bir dize ise de atabilir . Garip şey, Guzzle bağlantı zaman aşımı hatasına atıldığından RejectExceptionziyade RequestExceptionalmanızdır ConnectException. Her neyse, RejectExceptionSentry'deki yığın izlemenizden geçmeniz ve reject()Promise'da yöntemin nerede çağrıldığını bulmanız için bir neden bulabilirsiniz .


1

Cevabımın başlangıcı olarak yorum bölümündeki yazarla tartışma:

Soru:

Özel guzzle ara katman yazılımlarınız var mı (ipucu: HandlerStack)?

Yazarın cevabı:

Evet çeşitli. Ancak ara katman yazılımı temelde bir istek / yanıt değiştiricidir, hatta orada yaptığım guzzle istekleri senkronize olarak yapılır.


Buna göre işte benim tezim:

Ara katman yazılımlarınızdan birinin içinde guzzle adı verilen bir zaman aşımı var. Şimdi tekrarlanabilir bir durum uygulamaya çalışalım.

Burada guzzle'ı çağıran ve alt çağrının istisna mesajı ile bir ret hatası döndüren özel bir ara katman yazılımımız var. Oldukça zor, çünkü dahili hata işleme nedeniyle yığın izinde görünmez oluyor.

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);
        };
    };
}

Bu, nasıl kullanabileceğinizi gösteren bir test örneğidir:

$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());
}

Buna karşı bir test yaptığım anda alıyorum

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

Yani ana namlu çağrınız başarısız gibi görünüyor ama gerçekte başarısız olan alt çağrı.

Bunun, sorununuzu tanımlamanıza yardımcı olup olmadığını bize bildirin. Ayrıca biraz daha hata ayıklamak için midwares paylaşmak eğer çok takdir ediyorum.


Haklı görünüyorsun! Ben o ara katman bir yerde rejection_for($e->getMessage())yerine çağırıyordu rejection_for($e). Varsayılan ara katman yazılımı için orijinal kaynağa bakıyordum (burada olduğu gibi: github.com/guzzle/guzzle/blob/master/src/Middleware.php#L106 ), ancak rejection_for($e)bunun yerine neden olduğunu tam olarak söyleyemedim throw $e. Testcaseime göre aynı şekilde basamaklandırılıyor gibi görünüyor. Basitleştirilmiş bir test çantası için orijinal gönderiye bakın.
Alev

1
@ Alev sevindim size yardımcı olabilir :) İkinci sorunuza göre: Aralarında bir fark varsa. Bu gerçekten kullanım senaryosuna bağlı. Belirli bir senaryoda (kullanılan istisna sınıfı hariç) herhangi bir fark yaratmaz, çünkü sadece tek bir çağrınız vardır. Aynı anda birden fazla ve eşzamansız çağrıya geçmeyi düşünüyorsanız, diğer istekler hala çalışırken kod kesintilerini önlemek için söz vermeyi düşünmelisiniz. Cevabımı kabul ettirmek için daha fazla bilgiye ihtiyacınız varsa, lütfen bana bildirin :)
Christoph Kluge

0

Merhaba Sorununuzu çözüp çözmediğinizi anlamadım.

Hata günlüğünün ne olduğunu göndermenizi istiyorum. Hem PHP'de hem de sunucunuzun hata günlüğünde arama yapın

Görüşlerinizi bekliyorum


1
İstisna yukarıda zaten yayınlanmıştır, bir arka plan işleminden geldiği ve gönderen satırdan daha fazla bir şey yoktur $client->request('GET', ...)(sadece normal bir guzzle istemcisi).
Alev

0

Bu, ortamınızda ara sıra gerçekleştiğinden ve RejectionException(en azından yapamadım) atmayı çoğaltmak zor catcholduğundan, kodunuza başka bir blok ekleyebilir misiniz , aşağıya bakın:

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.
}

Size ve bize bunun neden ve ne zaman gerçekleştiği hakkında bazı fikirler vermelidir.


ne yazık ki öyle değil. Yığın izini Sentry'de aldım çünkü onu yakalamadan, sonunda Laravel İstisna işleyicisine ulaşıyor (ve Sentry'e gönderiliyor). Yığın izi sadece Guzzle kütüphanesinin derinliklerine işaret ediyor, ancak neden bir söz aldığını anlayamıyorum.
Alev

Neden söz verdiği varsayımıyla ilgili başka bir cevabımı görün: stackoverflow.com/a/60498078/1568963
Vladimir
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.