PHP'de cURL ile Dış API Çağırma ve Hata Yönetimi

PHP'de cURL ile Dış API Çağırma ve Hata Yönetimi

6 dakika okuma

Dış servis çağrıları, bir uygulamanın denetimi dışındaki tek bölümüdür. Servis yavaşladığında ya da yanıt vermediğinde sorun kendi uygulamanıza yansır: istekler birikir, işlem havuzu dolar ve sayfa üretimi tamamen durur. Bu yüzden her dış çağrının zaman sınırı, hata davranışı ve başarısızlık planı önceden tanımlanmalıdır.

Bu bölümde cURL ile istek gönderme, zaman aşımı ayarları, hata ayrımı, yeniden deneme düzeni ve çağrıların test edilebilir hale getirilmesi ele alınıyor.

Temel istek

Varsayılan davranışta yanıt doğrudan çıktıya yazılır. Yanıtın değişkene alınması için ilgili seçenek açılmalıdır. Aşağıdaki örnek, en az ayarla çalışan bir okuma isteğidir.

PHP15 satır
$ch = curl_init('https://ornek.test/api/v1/urunler');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,     // yanıt değişkene döner
    CURLOPT_CONNECTTIMEOUT => 3,        // bağlantı kurma sınırı
    CURLOPT_TIMEOUT => 10,              // toplam süre sınırı
    CURLOPT_FOLLOWLOCATION => false,    // yönlendirmeler denetimli ele alınır
    CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);

$govde = curl_exec($ch);
$kod = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$hata = curl_errno($ch);

curl_close($ch);

Zaman aşımı ayarları isteğe bağlı değildir. Tanımlanmadığında istek, sunucu yanıt verene kadar bekler ve bu süre dakikalarla ölçülebilir. Bağlantı süresi ile toplam süre ayrı ayrı sınırlanmalıdır.

Hata türlerini ayırmak

İki farklı başarısızlık vardır ve karıştırılmamalıdır. Aktarım düzeyindeki hatalar sunucuya hiç ulaşılamadığını gösterir; bunlar yeniden denenebilir. Yanıt kodundan gelen hatalar ise sunucunun isteği anladığını ancak reddettiğini gösterir ve genellikle yeniden denemek işe yaramaz.

PHP14 satır
if ($hata !== CURLE_OK) {
    // ağ, DNS, zaman aşımı: sonra tekrar denenebilir
    throw new ServisUlasilamadi(curl_strerror($hata));
}

if ($kod === 429 || $kod >= 500) {
    // sunucu geçici olarak yetişemiyor: beklenip tekrar denenir
    throw new ServisMesgul("Yanıt kodu {$kod}");
}

if ($kod >= 400) {
    // istek hatalı: aynı istek tekrar gönderilirse aynı sonuç alınır
    throw new IstekReddedildi("Yanıt kodu {$kod}", $kod);
}

Zaman aşımı süreleri, çağrılan servisin gerçek davranışına göre belirlenir. Ödeme sağlayıcısı gibi yavaş yanıt veren servislerde süre uzun tutulabilir, ancak bu durumda çağrının web isteği içinde değil arka planda yapılması düşünülmelidir. Kullanıcının on beş saniye bekletildiği bir akış, teknik olarak çalışsa bile kullanılabilir değildir.

Yanıt gövdesi de doğrulanmalıdır. Sunucunun 200 kodu döndürmesi, gövdenin beklenen yapıda olduğunu göstermez; çözümleme ve alan denetimi ayrıca yapılır.

Veri göndermek

PHP17 satır
$govde = json_encode($veri, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);

$ch = curl_init('https://ornek.test/api/v1/siparisler');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $govde,
    CURLOPT_CONNECTTIMEOUT => 3,
    CURLOPT_TIMEOUT => 15,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Accept: application/json',
        'Authorization: Bearer ' . $belirtec,
        'Idempotency-Key: ' . $islemAnahtari,
    ],
]);

Gönderilecek veri form biçimindeyse dizi doğrudan verilebilir; bu durumda içerik türü kendiliğinden ayarlanır. JSON gönderilirken ise metin olarak kodlanır ve içerik türü başlığı açıkça yazılır. İkisinin karıştırılması, sunucunun gövdeyi boş görmesine yol açan yaygın bir hatadır.

Yazma isteklerinde tekrar güvenliği önemlidir. Zaman aşımına uğrayan bir istek sunucuya ulaşmış olabilir; yeniden gönderildiğinde aynı işlem ikinci kez gerçekleşir. Servis destekliyorsa benzersiz bir işlem anahtarı gönderilir, desteklemiyorsa sonucun sorgulanabileceği bir yol bulunmalıdır.

Güvenlik ayarları

Sertifika doğrulaması hiçbir koşulda kapatılmaz. Doğrulamayı kapatmak, araya girme saldırılarına kapıyı açar ve şifreli bağlantının sağladığı güvencenin tamamını ortadan kaldırır. Geliştirme ortamında sertifika sorunu yaşanıyorsa çözüm, doğru sertifika paketini tanımlamaktır.

PHP5 satır
curl_setopt_array($ch, [
    CURLOPT_SSL_VERIFYPEER => true,     // varsayılan değer, kapatılmaz
    CURLOPT_SSL_VERIFYHOST => 2,
    CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt',
]);

Kullanıcıdan gelen bir adrese istek atılması gerekiyorsa, adresin iç ağ kaynaklarına yönlendirilmediği doğrulanmalıdır. Yönlendirmelerin izlenmesinin kapatılması ve hedef adresin çözümlenip denetlenmesi bu saldırıya karşı temel önlemdir.

Yeniden deneme düzeni

Geçici hatalarda hemen tekrar denemek, zaten zorlanan bir servisi daha da yorar. Bekleme süresi her denemede artırılır ve üzerine küçük bir rastgele sapma eklenir; böylece aynı anda başarısız olan istekler aynı anda tekrar denemez.

PHP17 satır
function yenidenDene(callable $islem, int $azami = 3): mixed
{
    $deneme = 0;

    while (true) {
        try {
            return $islem();
        } catch (ServisUlasilamadi|ServisMesgul $e) {
            if (++$deneme >= $azami) {
                throw $e;
            }

            $bekleme = (2 ** $deneme) + random_int(0, 1000) / 1000;
            usleep((int) ($bekleme * 1_000_000));
        }
    }
}

Deneme sayısı sınırsız olmamalıdır. Belirli bir eşikten sonra çağrı durdurulur ve istek kuyruğa alınır ya da kullanıcıya sonucun daha sonra bildirileceği söylenir. Sürekli başarısız olan bir servise istek göndermeyi geçici olarak kesen devre kesici düzeni, bu davranışın yaygın adıdır.

Eş zamanlı istekler

Birbirinden bağımsız birkaç servise art arda istek göndermek, süreleri toplar. Üç servisin her biri bir saniye sürüyorsa toplam üç saniye beklenir. İstekler eş zamanlı gönderildiğinde ise toplam süre en yavaş isteğe iner.

PHP35 satır
$coklu = curl_multi_init();
$isteklar = [];

foreach ($adresler as $ad => $adres) {
    $ch = curl_init($adres);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 3,
        CURLOPT_TIMEOUT => 8,
    ]);

    curl_multi_add_handle($coklu, $ch);
    $isteklar[$ad] = $ch;
}

do {
    $durum = curl_multi_exec($coklu, $calisan);
    if ($calisan) {
        curl_multi_select($coklu);
    }
} while ($calisan && $durum === CURLM_OK);

$yanitlar = [];

foreach ($isteklar as $ad => $ch) {
    $yanitlar[$ad] = [
        'govde' => curl_multi_getcontent($ch),
        'kod' => curl_getinfo($ch, CURLINFO_RESPONSE_CODE),
    ];

    curl_multi_remove_handle($coklu, $ch);
    curl_close($ch);
}

curl_multi_close($coklu);

Eş zamanlı çağrılarda toplam sınır da düşünülmelidir. Aynı anda açılan çok sayıda bağlantı, hem karşı servisi zorlar hem de kendi sunucunuzda kaynak tüketir; eş zamanlı istek sayısı makul bir değerle sınırlanır.

Eş zamanlı istekler yalnızca aralarında sıra bağımlılığı olmayan çağrılar için uygundur. Birinin sonucu diğerinin girdisi oluyorsa sıralı gitmek zorunludur; bu durumda kazanç, gereksiz çağrıları elemekten gelir.

Çağrıları yalıtmak

cURL çağrılarının iş mantığının içine dağılması iki soruna yol açar: aynı ayarlar tekrarlanır ve kod test edilemez hale gelir. Çağrılar bir arayüzün arkasına alındığında testlerde gerçek servis yerine hazır yanıt döndüren bir uygulama kullanılabilir.

PHP27 satır
interface HttpIstemci
{
    public function gonder(string $yontem, string $adres, array $secenek = []): Yanit;
}

final class CurlIstemci implements HttpIstemci
{
    public function __construct(
        private readonly int $baglantiSaniye = 3,
        private readonly int $toplamSaniye = 10,
    ) {}

    public function gonder(string $yontem, string $adres, array $secenek = []): Yanit
    {
        // ortak ayarlar tek yerde toplanır
    }
}

final class SahteIstemci implements HttpIstemci
{
    public function __construct(private readonly Yanit $hazir) {}

    public function gonder(string $yontem, string $adres, array $secenek = []): Yanit
    {
        return $this->hazir;
    }
}

Aynı yapı, istek ve yanıtların günlüğe yazılmasını da tek noktadan yönetmeyi sağlar. Kayıtlara gizli başlıkların yazılmaması için yetkilendirme başlıklarının maskelenmesi gerekir.

Kontrol listesi

  • Her çağrıda bağlantı ve toplam süre sınırı tanımlı.
  • Aktarım hatası ile yanıt kodu hatası ayrı ele alınıyor.
  • Sertifika doğrulaması açık.
  • Yazma isteklerinde tekrar güvenliği sağlanmış.
  • Yeniden deneme sayısı sınırlı ve bekleme süresi artan biçimde.
  • Çağrılar bir arayüz arkasında; testlerde sahte uygulama kullanılabiliyor.
  • Günlüklerde yetkilendirme başlıkları maskeleniyor.

İlgili yazılar

PHP eğitim serisi

Bu yazı, temel sözdiziminden üretim ortamına kadar ilerleyen 25 bölümlük serinin bir parçası. Bölümler sırayla okunacak biçimde hazırlandı, ancak her biri tek başına da kullanılabilir.

Temeller

  1. Kurulum ve temel sözdizimi
  2. Döngüler ve akış kontrolü
  3. Diziler
  4. Fonksiyonlar
  5. String fonksiyonları
  6. Süper globaller
  7. Form işleme
  8. Dosya işlemleri

Dilin ayrıntıları

  1. Tip sistemi ve strict_types
  2. Hata ve istisna yönetimi
  3. Düzenli ifadeler
  4. Tarih ve saat işlemleri

Nesne yönelimli PHP

  1. Sınıf ve nesne
  2. Kalıtım, arayüz ve trait
  3. Statik üyeler, sabitler ve enum
  4. Namespace, Composer ve autoload

Veri ve güvenlik

  1. PDO ile veritabanı işlemleri
  2. Oturum yönetimi ve kimlik doğrulama
  3. Güvenli çıktı ve XSS
  4. JSON ile veri değişimi

Dış dünya ve üretim

  1. cURL ile dış API çağırma (bu yazı)
  2. Kendi REST API'ni yazmak
  3. PHPUnit ile birim test
  4. Hata ayıklama ve performans ölçümü
  5. Üretime alma ve güvenlik

Yorumlar

Bu form reCAPTCHA ile korunur; Google Gizlilik Politikası ve Hizmet Şartları geçerlidir.

İlk yorumu sen yap.

İlgili Yazılar