Skip to content

Bölüm 11 / 14

HttpClient

IHttpClientFactory, dış servis çağırma, Polly ile yeniden deneme.

11 dakikalık okuma

4. hattın oluşturucu desenler dersinde IHttpClientFactory'yi "fabrikanın gerçek bir üretim problemini çözmesi" diye örnek göstermiş, yapısal desenler dersinde DelegatingHandler zincirinin dekoratör olduğunu söylemiş ve ikisinin ayrıntısını bu derse bırakmıştık. Konu göründüğünden geniştir: HttpClient'ın yanlış kullanımı, üretimde uygulama açılışta çalışıp bir süre sonra bağlantı kuramaz hâle gelen hataların en yaygın sebebidir.

01Ne öğreneceksin

  • İki karşıt tuzak: soket tükenmesi ve bayat DNS
  • IHttpClientFactory'nin handler havuzu ve handler yaşam süresi
  • Üç kayıt biçimi: temel, adlandırılmış, tipli istemci
  • Tipli istemcinin yaşam süresi ve esir bağımlılık tuzağı
  • JSON yardımcıları, akışlı okuma ve iptal jetonu
  • DelegatingHandler zinciri: kimlik, korelasyon, loglama
  • Zaman aşımı, yeniden deneme, devre kesici ve idempotentlik
  • Yeniden deneme fırtınası ve Retry-After

02İki karşıt tuzak

HttpClient IDisposable uyguladığı için ilk sezgi onu using ile kullanmaktır. Bu sezgi yanlıştır:

// Tuzak 1 — her çağrıda yeni istemci
using var istemci = new HttpClient();          // her Dispose sonrası soket TIME_WAIT'te bekler
var yanit = await istemci.GetAsync(adres);     // yük altında: SocketException, "adres kullanımda"

// Tuzak 2 — sonsuza kadar yaşayan tek istemci
static readonly HttpClient _istemci = new();   // DNS değişikliğini fark etmez, bağlantı bayatlar

Birinci tuzakta sorun nesne değil, altındaki sokettir. Kapatılan bir TCP bağlantısı işletim sisteminde bir süre (tipik olarak dakikalarca) TIME_WAIT durumunda bekler; saniyede yüzlerce istek yapan bir servis kısa sürede kullanılabilir port aralığını tüketir. Belirtisi tanıdıktır: uygulama açılışta sorunsuz çalışır, yük arttıkça bağlantı hataları başlar.

İkinci tuzak bunun aşırı düzeltmesidir. Tek bir istemci soket sorununu çözer ama açtığı bağlantıları süresiz kullanır ve DNS değişikliğini görmez: hedef servis yeni bir IP'ye taşındığında (bulut ortamlarında olağan) istemci eski adrese gitmeye devam eder.

IHttpClientFactory ikisini birden çözer: HttpClient nesnesi kısa ömürlüdür, ama altındaki HttpMessageHandler havuzdan gelir ve paylaşılır. Handler'lar varsayılan olarak 2 dakika yaşar, sonra havuzdan çıkarılır — bu süre, DNS'in yeniden çözülmesini garanti eden penceredir. Süre ayarlanabilir:

builder.Services.AddHttpClient("odeme")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));      // varsayılan 2 dk

Kıdemli seviyede bilinen bir alternatif daha vardır: tek bir uzun ömürlü istemci kullanıp bağlantı yenilemeyi SocketsHttpHandler.PooledConnectionLifetime ile yönetmek. Fabrika ile çözülen problemi doğrudan çözer ve fabrikanın getirdiği düzeni (adlandırılmış yapılandırma, handler zinciri) istemeyen basit senaryolarda tercih edilebilir. Bir servis içinde ikisinden biri seçilir; karıştırmak yapılandırmayı izlenemez hâle getirir.

03Kayıt biçimleri ve tipli istemci

Üç biçim vardır; üretimde tipli istemci tercih edilir.

// 1) Temel — fabrikadan adsız istemci
builder.Services.AddHttpClient();
public sealed class X(IHttpClientFactory fabrika)
{
    public async Task Cagir() { var c = fabrika.CreateClient(); /* ... */ }
}

// 2) Adlandırılmış — yapılandırma ada bağlı, çağıran adı bilmek zorunda
builder.Services.AddHttpClient("odeme", c =>
{
    c.BaseAddress = new Uri("https://odeme.ornek.local/");
    c.Timeout = TimeSpan.FromSeconds(10);
});

// 3) Tipli — yapılandırma ve kullanım tek tipte; ad yazım hatası imkânsız
builder.Services.AddHttpClient<OdemeIstemcisi>((sp, c) =>
{
    var ayar = sp.GetRequiredService<IOptions<OdemeAyarlari>>().Value;   // 3. ders
    c.BaseAddress = new Uri(ayar.TabanAdres);
    c.Timeout = TimeSpan.FromSeconds(ayar.ZamanAsimiSaniye);
    c.DefaultRequestHeaders.Add("X-Kaynak-Uygulama", "siparis-servisi");
});

Taban adres ve zaman aşımı gibi değerlerin koda gömülmemesi, 3. dersteki options deseninin doğrudan uygulamasıdır: ortam değiştiğinde (geliştirme, test, üretim) yalnız yapılandırma değişir. Tipli istemci sınıfı sade kalır:

public sealed class OdemeIstemcisi(HttpClient http, ILogger<OdemeIstemcisi> log)
{
    public async Task<OdemeSonucu> Tahsil(OdemeIstegi istek, CancellationToken ct)
    {
        var yanit = await http.PostAsJsonAsync("tahsilatlar", istek, ct);

        if (yanit.StatusCode is HttpStatusCode.Conflict)
            return OdemeSonucu.ZatenIslenmis;                       // beklenen iş durumu

        yanit.EnsureSuccessStatusCode();                            // beklenmeyen → istisna
        return await yanit.Content.ReadFromJsonAsync<OdemeSonucu>(ct)
               ?? throw new InvalidOperationException("Boş yanıt gövdesi.");
    }
}

Yaşam süresi tuzağı. Tipli istemci transient kaydedilir ve içindeki HttpClient'ın ömrü handler yaşam süresine bağlıdır. Bu tipi bir singleton'a enjekte edersen, 4. dersteki esir bağımlılık hatasını yaparsın: istemci uygulama boyunca yaşar, handler asla yenilenmez ve DNS sorunu geri döner. Kural: tipli istemci scoped ya da transient bileşenlerden kullanılır; singleton bir servis gerekiyorsa içine IHttpClientFactory enjekte edilir ve istemci her çağrıda yeniden alınır.

04İstek, yanıt ve iptal

JSON uzantıları (System.Net.Http.Json) çoğu işi kısaltır: GetFromJsonAsync<T>, PostAsJsonAsync, ReadFromJsonAsync<T>. Bilinmesi gereken davranış farkları:

  • EnsureSuccessStatusCode ile IsSuccessStatusCode: birincisi başarısız kodda istisna fırlatır. Beklenen iş durumlarını (404 "kayıt yok", 409 "zaten işlenmiş") istisnaya çevirmek pahalıdır ve akışı bozar; bunlar StatusCode kontrolüyle ele alınır, gerçekten beklenmeyen durumlar istisnaya bırakılır.
  • Büyük yanıtlar: GetAsync varsayılan olarak gövdeyi tamamen tamponlar. Büyük dosya ya da uzun liste çekiyorsan HttpCompletionOption.ResponseHeadersRead ile başlıklar gelir gelmez dönülür ve gövde akış olarak okunur; bellek kullanımı sabit kalır.
  • CancellationToken her çağrıya verilir. Denetleyicideki jeton (8. ders) buraya kadar taşınmalıdır: istemci bağlantıyı keserse dış çağrı da iptal edilir. Taşınmazsa istemci gitmiş olsa bile sunucu dış servisi beklemeye devam eder.
  • Zaman aşımı iptalden ayırt edilmelidir. HttpClient.Timeout aşıldığında fırlatılan TaskCanceledException'ın iç istisnası TimeoutException'dır; kullanıcı iptalinde ise iç istisna yoktur ve jeton işaretlidir. Loglarda bu ikisini ayırmazsan "çok fazla iptal var" diye yanlış bir tablo okursun.
try
{
    using var kaynak = CancellationTokenSource.CreateLinkedTokenSource(ct);
    kaynak.CancelAfter(TimeSpan.FromSeconds(3));            // bu çağrıya özel süre
    var yanit = await http.GetAsync(yol, HttpCompletionOption.ResponseHeadersRead, kaynak.Token);
    await using var akis = await yanit.Content.ReadAsStreamAsync(kaynak.Token);
    // akış üzerinden işle
}
catch (TaskCanceledException e) when (e.InnerException is TimeoutException)
{
    log.LogWarning("Dış servis zaman aşımına uğradı: {Yol}", yol);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
    // istemci vazgeçti; hata değil
}

05DelegatingHandler zinciri

4. hatta söz verilen dekoratör karşılığı budur: her handler bir sonrakini sarar, isteğin gidişinde ve yanıtın dönüşünde iş yapabilir — middleware boru hattının istemci tarafındaki ikizi.

public sealed class KorelasyonHandler(IHttpContextAccessor erisimci) : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage istek, CancellationToken ct)
    {
        var iz = erisimci.HttpContext?.TraceIdentifier;
        if (iz is not null) istek.Headers.TryAddWithoutValidation("X-Korelasyon-No", iz);
        return await base.SendAsync(istek, ct);        // zincirin devamı
    }
}

builder.Services.AddTransient<KorelasyonHandler>();
builder.Services.AddHttpClient<OdemeIstemcisi>(...)
    .AddHttpMessageHandler<KorelasyonHandler>()        // sıra önemlidir: en dıştan içe
    .AddHttpMessageHandler<KimlikHandler>();

Tipik kullanımlar: kimlik (jetonu alıp Authorization başlığına koymak, süresi dolunca yenilemek), korelasyon (istek kimliğini alt servislere taşımak — 19. hattaki izlenebilirliğin temeli), loglama ve başlık normalleştirme. Sıra, tıpkı middleware'de olduğu gibi ekleme sırasıdır ve anlamı değiştirir: loglama handler'ı kimlik handler'ının dışındaysa jetonsuz isteği loglar, içindeyse jetonlu — ve jeton loglanmamalıdır (5. ders).

İki tuzak. Birincisi: handler'lar havuzlanır ve paylaşılır, bu yüzden örnek alanında istek verisi tutulmaz. İkincisi: handler singleton gibi yaşadığından kurucusuna scoped servis alınamaz — IHttpContextAccessor bu kısıtı aşmak için vardır, çünkü kendisi singleton'dır ve o anki isteği AsyncLocal üzerinden bulur. Bu erişimciyi bir arka plan işinden çağırırsan HttpContext null'dur; 12. derste bu ayrıntı önemli olacak.

06Dayanıklılık: yeniden deneme ve devre kesici

Ağ üzerinden yapılan her çağrı başarısız olabilir. Dört temel politika vardır ve birlikte kullanılırlar:

PolitikaNeyi çözerRiski
Zaman aşımı Yanıt vermeyen servisin kaynağı tutmasını Çok kısa süre → gereksiz hata
Yeniden deneme Geçici hatalar (ağ titremesi, 503) Yükü katlar; idempotent olmayan işlemi bozar
Devre kesici Çöken servise yığılmayı Yanlış eşikte sağlıklı trafiği keser
Eşzamanlılık sınırı Tek bir bağımlılığın tüm havuzu tüketmesini Dar sınır → kuyruk

.NET tarafında bunlar Polly ile ve .NET 8'den beri Microsoft.Extensions.Http.Resilience paketiyle kurulur. Standart yığın tek satırla eklenir:

builder.Services.AddHttpClient<OdemeIstemcisi>(...)
    .AddStandardResilienceHandler(o =>
    {
        o.Retry.MaxRetryAttempts = 3;
        o.Retry.UseJitter = true;                          // eşzamanlı denemeleri dağıtır
        o.AttemptTimeout.Timeout  = TimeSpan.FromSeconds(3);
        o.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(12);
        o.CircuitBreaker.FailureRatio = 0.5;
    });

Üç kural, bu konudaki kıdem ölçütüdür:

  1. Yalnız idempotent işlem yeniden denenir. GET, PUT ve DELETE tanımı gereği yinelenebilir; POST değildir. "Ödeme al" isteğini körlemesine yeniden denemek çift tahsilat üretir — ilk istek aslında başarılı olup yanıt yolda kaybolmuş olabilir. Çözüm idempotentlik anahtarıdır: istemci her mantıksal işlem için benzersiz bir anahtar üretir, sunucu aynı anahtarla gelen ikinci isteği tekrar işlemez, ilk sonucu döndürür.
  2. Üstel geri çekilme + rastgelelik (jitter) şart. Sabit aralıklı yeniden deneme, çöken bir servis toparlanmaya çalışırken tüm istemcilerin aynı anda üstüne gitmesine yol açar — yeniden deneme fırtınası. Süre üstel artmalı ve üzerine rastgele bir pay eklenmelidir. Ayrıca sunucu Retry-After başlığı gönderiyorsa (429, 503) ona uyulur.
  3. Yeniden deneme katmanları çarpışır. Hem istemcide hem ara katmanda hem de çağıran serviste üçer deneme varsa, tek bir kullanıcı isteği alt servise 27 çağrı olarak gider. Yeniden deneme tek bir katmanda, tercihen çağrının kaynağına en yakın yerde yapılır.

Devre kesicinin mantığı da tanımlanmalıdır: hata oranı eşiği aşınca devre açılır ve çağrılar hedefe hiç gitmeden hızlıca reddedilir; bir süre sonra yarı açık duruma geçip tek bir deneme isteği gönderilir, başarılıysa devre kapanır. Amaç, çöken servisi rahat bırakmak ve çağıranın iş parçacıklarını boşuna bekletmemektir. Bu politikaların dağıtık sistem bağlamındaki tam tartışması 19. hattaki dayanıklılık dersindedir; burada önemli olan, dış çağrının her zaman bir zaman aşımı ve bir hata planıyla yazılmasıdır.

Son olarak izlenebilirlik: IHttpClientFactory ile yapılan çağrılar kendiliğinden loglanır ve Activity üzerinden izlere bağlanır (5. ders, 19. hat). Dış çağrının süresi, durum kodu ve yeniden deneme sayısı ölçülmezse yavaşlığın kaynağı kendi kodunda mı yoksa bağımlılıkta mı olduğu bilinemez.

07Mini alıştırma — Kırılgan bağımlılık

Yaklaşık 20 dakika · iki uygulama (biri "dış servis")

  1. Sahte dış servis: ikinci bir web uygulaması yaz; bir ucu isteklerin yarısında 500 dönsün, bir ucu 5 saniye beklesin, bir ucu 429 + Retry-After: 2 dönsün.
  2. Tipli istemci: ilk uygulamada AddHttpClient<T> ile tipli istemci kur; taban adresi ve zaman aşımını appsettings.json'dan IOptions ile oku.
  3. Zaman aşımı ayrımı: 5 saniyelik uca 2 saniyelik zaman aşımıyla git. Yakaladığın istisnanın tipi ve InnerException'ı ne? İstemciyi durdurup (isteği iptal ederek) aynı çağrıyı tekrarla ve farkı yaz.
  4. Handler zinciri: korelasyon başlığı ekleyen bir DelegatingHandler yaz; dış serviste başlığın geldiğini doğrula.
  5. Sıra etkisi: bir loglama handler'ı ekle ve zincirde kimlik handler'ının önüne, sonra arkasına al. Loglanan başlıklar nasıl değişti?
  6. Yeniden deneme: AddStandardResilienceHandler ekle, %50 hata veren uca 20 istek gönder ve dış servisin gördüğü toplam istek sayısını say. Beklediğin sayı ile gerçek sayı uyuşuyor mu?
  7. İdempotentlik: aynı yeniden denemeyi bir POST ucuna uygula; dış serviste kaç kayıt oluştu? Sonra istekleri bir idempotentlik anahtarıyla gönder ve sunucuda aynı anahtarı ikinci kez işlememeyi uygula.
  8. Soket kanıtı (isteğe bağlı): döngüde new HttpClient() ile 500 istek yap ve işletim sisteminde TIME_WAIT soketlerin arttığını gör (Linux'ta ss -s, Windows'ta netstat -an). Sonra fabrikayla tekrarla.

08Kendini sına

new HttpClient() neden sorunludur, IHttpClientFactory ne çözer?

İki karşıt tuzak vardır. Her çağrıda yeni istemci üretip Dispose etmek, altındaki TCP bağlantısını kapatır ve soket bir süre TIME_WAIT'te bekler; yoğun serviste kullanılabilir port aralığı tükenir — belirtisi, uygulamanın açılışta çalışıp yük arttıkça bağlantı hatası vermesidir. Bunu aşırı düzelten statik tek istemci ise açtığı bağlantıları süresiz kullanır ve DNS değişikliğini görmez; hedef yeni IP'ye taşındığında eski adrese gitmeye devam eder. IHttpClientFactory ikisini birden çözer: HttpClient nesnesi kısa ömürlüdür ama altındaki HttpMessageHandler havuzdan gelir ve paylaşılır; handler varsayılan olarak iki dakika yaşayıp havuzdan çıkarılır, böylece DNS yeniden çözülür. Alternatif olarak tek uzun ömürlü istemci + SocketsHttpHandler.PooledConnectionLifetime kullanılabilir; ikisinden biri seçilir, karıştırılmaz.

Tipli istemci nedir, yaşam süresi neden önemlidir?

Tipli istemci, AddHttpClient<T> ile kaydedilen ve kurucusundan HttpClient alan bir sınıftır; yapılandırma (taban adres, zaman aşımı, varsayılan başlıklar) ile kullanım aynı tipte toplanır ve adlandırılmış istemcideki "ad yazım hatası" riski ortadan kalkar. Taban adres gibi değerler kod yerine yapılandırmadan, IOptions ile okunur. Yaşam süresi kritiktir: tipli istemci transient kaydedilir ve içindeki HttpClient'ın ömrü handler yaşam süresine bağlıdır. Bu tipi bir singleton'a enjekte etmek esir bağımlılıktır: istemci uygulama boyunca yaşar, handler asla yenilenmez ve DNS sorunu geri döner. Singleton bir servis dış çağrı yapacaksa içine IHttpClientFactory enjekte edilir ve istemci her çağrıda yeniden alınır.

DelegatingHandler ne işe yarar, hangi tuzakları vardır?

İstemci tarafındaki dekoratör zinciridir: her handler bir sonrakini sarar, isteğin gidişinde ve yanıtın dönüşünde iş yapabilir — middleware boru hattının ikizidir. Tipik kullanımlar kimlik jetonu eklemek ve yenilemek, korelasyon/izleme kimliğini alt servislere taşımak, loglama ve başlık normalleştirmedir; zincire AddHttpMessageHandler ile takılır ve ekleme sırası anlamı değiştirir — loglama kimlikten önce mi sonra mı çalışıyor, jetonlu isteği mi jetonsuzu mu görüyor (jeton loglanmamalıdır). İki tuzak: handler'lar havuzlanıp paylaşıldığı için örnek alanında isteğe özgü durum tutulmaz; ve handler singleton gibi yaşadığından kurucusuna scoped servis alınamaz — o anki isteğe erişmek için singleton olan IHttpContextAccessor kullanılır, ama arka plan işlerinde HttpContext null olur.

Dış servis çağrısını dayanıklı hâle nasıl getirirsin?

Dört politika birlikte kullanılır: zaman aşımı (yanıt vermeyen servisin kaynağı tutmasını engeller), yeniden deneme (geçici hatalar için), devre kesici (çöken servise yığılmayı keser; açık → yarı açık → kapalı döngüsüyle çalışır) ve eşzamanlılık sınırı. .NET'te bunlar Polly ile ya da AddStandardResilienceHandler ile kurulur. Üç kural: (1) yalnız idempotent işlem yeniden denenir — GET/PUT/DELETE yinelenebilir, POST değildir; ödeme gibi işlemlerde körlemesine deneme çift tahsilat üretir, çözüm idempotentlik anahtarıdır. (2) Geri çekilme üstel olmalı ve rastgelelik (jitter) içermelidir, yoksa çöken servis toparlanırken tüm istemciler aynı anda üstüne gider — yeniden deneme fırtınası; sunucu Retry-After gönderiyorsa ona uyulur. (3) Yeniden deneme katmanları çarpışır: üç katmanda üçer deneme, tek isteği 27 çağrıya çevirir; deneme tek katmanda yapılır. Ayrıca her dış çağrının süresi, durum kodu ve deneme sayısı ölçülür; aksi hâlde yavaşlığın kaynağı bilinemez.

09Sınav köşesi

Yazılıda nasıl sorulur

(1) Klasik soru: HttpClient neden using ile kullanılmamalıdır; soket tükenmesi nedir. (2) IHttpClientFactory hangi iki problemi çözer; handler yaşam süresi ne işe yarar. (3) Adlandırılmış ve tipli istemci farkı. (4) DelegatingHandler nedir, hangi desene karşılık gelir. (5) EnsureSuccessStatusCode ile IsSuccessStatusCode farkı. (6) Zaman aşımı ile iptal nasıl ayırt edilir. (7) Yeniden deneme hangi HTTP metotları için güvenlidir; idempotentlik nedir. (8) Devre kesici durumları. (9) Kod yazma: bir dış servisi çağıran tipli istemci ve bir handler yazınız. (10) Tipli istemciyi singleton'a enjekte etmenin sonucu.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Problem — "HttpClient'ta iki karşıt tuzak vardır: her çağrıda yeni istemci soketleri TIME_WAIT'te tüketir, statik tek istemci DNS değişikliğini kaçırır." (2) Çözüm — "IHttpClientFactory istemciyi kısa ömürlü tutar ama handler'ı havuzlar ve iki dakikada bir yeniler; ikisini birden çözer." (3) Uygulama — "tipli istemci kullanırım; taban adresi ve zaman aşımını IOptions'tan okurum, ortak başlık ve kimlik işini DelegatingHandler zincirine koyarım — bu zincir dekoratör desenidir." (4) Dayanıklılık — "her dış çağrının zaman aşımı, yeniden denemesi ve devre kesicisi olur; AddStandardResilienceHandler ile kurarım." (5) Kıdem vuruşu — "yeniden denemeyi yalnız idempotent işlemlerde yaparım; POST'u ancak idempotentlik anahtarıyla yeniden denerim, yoksa çift tahsilat riski var. Geri çekilmeyi üstel ve jitter'lı tutarım ve denemeyi tek katmanda yaparım — katmanlar çarpışırsa tek istek onlarca çağrıya dönüşür. Tipli istemciyi singleton'a enjekte etmem; bu esir bağımlılıktır ve handler yenilenmez."