Skip to content

Bölüm 6 / 13

HttpClient

Interceptor ve hata yönetimi.

13 dakikalık okuma

HTTP'yi 6. hatta protokol olarak, fetch'i 8. hatta elle öğrendin; 9. hatta "sınırda doğrula, içeride güven" ilkesini kurdun. Angular'ın HttpClient'ı bu üçünü birleştirir: istekleri Observable olarak döndürür (5. dersin dili), tip parametresi alır ve interceptor zinciriyle her isteğe tek noktadan müdahale ettirir. Bu ders istek atmayı, yükleniyor/hata/veri üçlüsünü tek akışta yönetmeyi ve hata sözleşmesini ekrana çevirmeyi işler — 5. dersin catchError senedi burada ödenir.

01Ne öğreneceksin

  • provideHttpClient kaydı ve get<T> ile tipli istek
  • "İstek neden gitmiyor / neden iki kez gitti?" — tembellik ve soğuk akışın HTTP hâli
  • get<T>'nin dürüstlük sınırı: tip parametresi kanıt değil beyandır
  • Yükleniyor / hata / veri üçlüsünü tek Observable'da modellemek
  • HttpErrorResponse anatomisi; catchError'ın yeri; retry'ın sınırı
  • Fonksiyonel interceptor: kimlik başlığı, günlükleme, sıra ve clone kuralı

02Kayıt ve ilk istek

HttpClient kendiliğinden hazır değildir; uygulama kökünde kaydedilir — 4. dersteki provider düşüncesinin ta kendisi:

// app.config.ts
import { provideHttpClient, withInterceptors } from "@angular/common/http";

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([sureOlcer])), // interceptor'lar 6. bölümde
  ],
};

Veri erişimi bileşene değil servise yazılır (4. dersin katman kuralı: bileşen → alan servisi → altyapı). Servis HttpClient'ı enjekte eder:

import { HttpClient } from "@angular/common/http";

@Injectable({ providedIn: "root" })
export class BasvuruServisi {
  private http = inject(HttpClient);
  private readonly adres = "/api/basvurular";

  hepsi(): Observable<Basvuru[]> {
    return this.http.get<BasvuruDto[]>(this.adres)
      .pipe(map((liste) => liste.map(dtodanBasvuruya)));
  }
  ekle(gov: YeniBasvuru) { return this.http.post<BasvuruDto>(this.adres, gov); }
  sil(id: number)        { return this.http.delete<void>(`${this.adres}/${id}`); }
}

Dönüş tipi Observable'dır ve 5. dersin iki kuralı burada hemen ısırır:

  • Tembellik: Observable abone olunana dek çalışmaz — this.http.get(...) yazıp subscribe (ya da async pipe) bağlamazsan istek ağa hiç çıkmaz. "Servisi çağırdım ama Network sekmesi boş" şikâyetinin cevabı neredeyse her zaman budur. Özellikle post/delete'te unutulur: okuma şablona akar da, "kaydet" çağrısı abonesiz kalır.
  • Soğukluk: her abonelik isteği baştan atar. Aynı akışa şablonda iki | async bağlarsan Network'te iki istek görürsün — 5. derste verilen sözün kanıtı. Çözüm oradan: as ile tek bağla ya da akışı shareReplay(1) ile paylaşıma aç.

Bir konfor: HTTP akışı tek değer yayınlayıp tamamlanır (complete). Yani tek seferlik bir isteğe abone olmak sızıntı biriktirmez; yine de bileşen, cevap gelmeden yıkılabileceği için görünüme akan her şeyde birinci tercih async pipe kalır. İnce bir ödül daha: async pipe bileşenle birlikte aboneliği bırakınca HttpClient isteği gerçekten iptal eder — kullanıcı sayfadan ayrılırsa cevabı kimsenin beklemediği istek ağda süründürülmez.

03get<T> dürüstlüğü ve istek seçenekleri

get<Basvuru[]> yazınca derleyici mutlu olur ama 9. hattın 9. dersini unutma: tip parametresi hiçbir şeyi doğrulamaz — çalışma anında tipler silinmiştir, bu bir gizli as'tir. Sunucu alan adını değiştirirse uygulama derlenmeye ve sessizce yanlış çalışmaya devam eder. Disiplin 9. hattan aynen taşınır:

  • DTO'yu gelen hâliyle dürüst tiple: tarih alanları string'dir, Date değil. JSON'da tarih tipi yoktur (6. hat, 2. ders).
  • DTO → iç model dönüşümünü tek dönüştürücü fonksiyondan geçir (dtodanBasvuruya); tarih orada Date'e çevrilir, alan yeniden adlandırmaları orada yapılır. Sunucu değişince tek dosya değişir.
  • Güvenlik kritik sınırlarda (ödeme, yetki) dönüşümün başına 9. hattın koruyucu fonksiyonlarını koy; sıradan listelerde çoğu ekip get<Dto> beyanı + tek dönüştürücüyle yetinir. Bunun bilinçli bir risk kararı olduğunu söyleyebilmek kıdem göstergesidir.

Sorgu parametreleri ve başlıklar seçenek nesnesiyle verilir; HttpParams'ın az bilinen tuzağı değişmezliğidir:

let params = new HttpParams();
params.set("durum", "acik");        // TUZAK: sonuç atanmadı — params HÂLÂ BOŞ
params = params.set("durum", "acik") // doğru: her set YENİ nesne döndürür
               .set("sayfa", "2");

this.http.get<BasvuruDto[]>(this.adres, { params });

3. dersteki değişmezlik sözleşmesi düşüncesi burada API tasarımına sinmiş: set yerinde değiştirmez, kopya döndürür. Dönüşü atamayan kod derlenir, çalışır ve parametresiz istek atar — hata mesajı yoktur, yalnız Network sekmesi gerçeği söyler.

04Üçlü durum: yükleniyor / hata / veri

Her uzak veri ekranının üç hâli vardır ve üçü de tasarlanmadıysa ekran yalan söyler: sonsuz boş sayfa (yükleniyor çizilmedi), sessiz başarısızlık (hata çizilmedi). 9. hattın 4. dersindeki ayırt edici birleşim, bu üçlünün doğal modelidir ve tek akışta kurulur:

type ListeDurumu =
  | { tur: "yukleniyor" }
  | { tur: "hata"; mesaj: string }
  | { tur: "hazir"; basvurular: Basvuru[] };

export class BasvuruListesi {
  private servis = inject(BasvuruServisi);

  durum$: Observable<ListeDurumu> = this.servis.hepsi().pipe(
    map((liste): ListeDurumu => ({ tur: "hazir", basvurular: liste })),
    startWith<ListeDurumu>({ tur: "yukleniyor" }),      // ilk kare: bekleme
    catchError((e) => of<ListeDurumu>({ tur: "hata", mesaj: kullaniciMesaji(e) })),
  );
}

Şablon 3. dersin @switch'iyle çizer — çelişkili hâl (hem yükleniyor hem dolu) tipte yazılamaz:

@switch ((durum$ | async)?.tur) {
  @case ("yukleniyor") { <p class="bekleme">Yükleniyor…</p> }
  @case ("hata")       { <p role="alert">{{ hataMesaji }}</p> }
  @case ("hazir")      { <!-- @for (b of ...; track b.id) --> }
}

(Pratikte @if (durum$ | async; as durum) ile tek bağlayıp durum.tur üstünde @switch kurulur — çift async tuzağına düşmeden.) Bu kalıbın kıymeti şurada: yükleniyor bayrağını elle true/false yapan kod, bir catch dalında bayrağı kapatmayı unutur ve ekran sonsuza dek "yükleniyor" der. Akış kalıbında bu sınıf hata yoktur — durum, isteğin yaşam döngüsünden türetilir.

05Hata yönetimi: HttpErrorResponse anatomisi

Başarısız istek, akışın error kanalına HttpErrorResponse düşürür. İlk iş teşhistir — status alanını 6. hattın 1. dersindeki dille oku:

statusAnlamıEkrana / koda düşen
0Cevap hiç gelmedi: ağ kesik, sunucu kapalı ya da CORS engeli (8. hat 17. ders)"Bağlantı kurulamadı" + tekrar dene; CORS ise konsol mesajını oku — sunucu tarafı iştir
400 / 422İstek gövdesi geçersiz — doğrulama hatalarıHata sözleşmesindeki alan hatalarını forma geri yaz (8. ders)
401 / 403Kimlik yok / yetki yokGirişe yönlendir / "yetkin yok" — ayrıntısı 13. hatta
404Kaynak yok"Bulunamadı" ekranı; listeye dönüş bağlantısı
5xxSunucu düştü — senin isteğin suçsuz olabilirGenel mesaj + tekrar dene; ayrıntıyı kullanıcıya dökme

Gövdede, 6. hattın 5. dersinde tasarladığın hata sözleşmesi (Problem Details) yatar: err.error alanından ayrıştırılır ve kullanıcı diline çevrilir — ham err.message asla ekrana basılmaz. Bu çeviri tek fonksiyonda toplanır (kullaniciMesaji), her bileşen kendi cümlesini uydurmaz.

catchError'ın yeri — 5. dersin kuralı HTTP'de kalıplaşır: sürekli yaşaması gereken akışlarda (arama kutusu) catchError iç akışa konur; yoksa ilk hata dış akışı öldürür ve arama tümden susar. Tek seferlik ekran yüklemesinde ise 4. bölümdeki gibi zincirin sonunda durum nesnesine çevrilir — orada akışın "ölmesi" sorun değildir, zaten tek değer taşıyacaktı.

retry disiplinli kullanılır:

this.http.get<BasvuruDto[]>(this.adres).pipe(
  retry({ count: 2, delay: 1000 }),   // yalnız OKUMADA; her denemede 1 sn bekle
)

Sınırını 6. hattan biliyorsun: GET idempotenttir, tekrarı zararsızdır; ama POST'u körlemesine tekrarlamak çift kayıt üretebilir. Kural: okuma isteklerinde az sayıda, gecikmeli tekrar; yazmalarda tekrar yok — gerekiyorsa sunucuyla idempotency anahtarı konuşulur (6. hat). Bir incelik daha: 4xx'i tekrarlamak anlamsızdır (istek bozuk — sunucu fikrini değiştirmeyecek); gerçek projede tekrar, status === 0 ve 5xx ile sınırlandırılır.

06Interceptor: her isteğin geçtiği kapı

Kimlik başlığı eklemek, süre ölçmek, genel hata günlüğü tutmak — bunları her servise kopyalamak DRY cinayetidir. HttpClient'ın cevabı interceptor'dır: her istek ve cevabın geçtiği ara katman. 5. hattın 7. dersindeki middleware boru hattının birebir istemci karşılığıdır: istek zincirde aşağı iner, cevap aynı zincirden yukarı çıkar; kayıt sırası çalışma sırasıdır.

import { HttpInterceptorFn } from "@angular/common/http";

export const kimlikEkleyici: HttpInterceptorFn = (req, next) => {
  const oturum = inject(OturumServisi);   // fonksiyonda inject() — 4. dersin senedi
  const jeton = oturum.jeton();
  if (!jeton) return next(req);

  // İstek nesnesi DEĞİŞMEZDİR: üzerine yazamazsın, clone şart
  const yenisi = req.clone({ setHeaders: { Authorization: `Bearer ${jeton}` } });
  return next(yenisi);
};

export const sureOlcer: HttpInterceptorFn = (req, next) => {
  const baslangic = performance.now();
  return next(req).pipe(
    finalize(() => console.debug(`${req.method} ${req.urlWithParams}:`,
      `${Math.round(performance.now() - baslangic)} ms`)),
  );
};

// kayıt: provideHttpClient(withInterceptors([kimlikEkleyici, sureOlcer]))

Üç noktanın altını çiz:

  • Fonksiyonel yazım + inject(): 4. dersteki tablonun "fonksiyonda kullanım" satırı burada meyve verir — sınıf yazmadan, kurucusuz, bağımlılık alan bir ara katman. (Eski kodda HttpInterceptor arayüzünü uygulayan sınıflar okuyacaksın; fikir aynıdır.)
  • clone zorunluluğu: istek nesnesi değişmezdir. req.headers = ... diye bir yol yoktur; değişiklik her zaman kopya üstünedir. Bu, aynı isteğin tekrar denenebilmesinin (retry, yönlendirme) ön koşuludur — 3. dersteki değişmezlik sözleşmesinin altyapı hâli.
  • Sıra önemlidir: dizideki sıra, isteğin katmanlardan geçiş sırasıdır. Süre ölçen interceptor kimlikten önce kaydedilirse kimlik ekleme süresini de ölçer; günlükleyen en dışta, kimlik ekleyen içte olsun istiyorsan diziyi ona göre diz — 5. hatta middleware sırası neden tartışılıyorsa burada da aynı sebep.

Jetonun nereden geldiği, nasıl saklandığı ve tazelendiği bu dersin sınırı dışında — OAuth2/OIDC ve saklama kararlarıyla birlikte 13. hattın konusu. Bugünlük kalıp net: kimlik başlığı bileşenlerde değil, tek interceptor'da eklenir.

07Üretim pratiği: adres, önbellek, iptal

  • API adresi koda gömülmez: 4. dersteki InjectionToken kalıbıyla (ya da ortam dosyasıyla) kökte bir kez verilir; servisler enjekte eder. 10-01'in kuralı burada da geçerli: pakete sır konmaz — istemciye giden her şey kullanıcının eline geçer; adres sır değildir ama anahtar/parola asla buraya yazılmaz. Ortam ve sır yönetiminin tamamı 13. hatta.
  • Önbellek bilinçli açılır: shareReplay(1) soğuk isteği paylaşıma açıp tekrar aboneye son cevabı verir — ekran içi çift istek biter. Ama bedava değil: veri bayatlar; "listeye kayıt ekledim, ekran eski listeyi gösteriyor" şikâyetinin kaynağı, ne zaman tazeleneceği düşünülmemiş shareReplay'dir. Yazma sonrası yeniden çekme ya da durumu serviste tutma (9. ders) çözümdür.
  • İptal bedavaya gelir, kullan: aboneliği bırakmak isteği iptal eder — bu yüzden 5. dersin arama kalıbındaki switchMap, HTTP'de yalnız "eski cevabı ele" değil, "eski isteği ağda da iptal et" demektir. 8. hatta AbortController ile elle yaptığın işin tamamı, operatör seçiminin yan ürünü oldu.
  • Bileşen HttpClient enjekte etmez: bileşende inject(HttpClient) gördüğün an refactoring sinyalidir — URL bilgisi, DTO dönüşümü ve hata çevirisi bileşene sızmış demektir. İstisnasız servis katmanından geçir.

08Mini alıştırma — Üçlü durum + interceptor

Yaklaşık 20 dakika · kurulum yok — StackBlitz'te Angular şablonu; veri kaynağı olarak jsonplaceholder.typicode.com (sahte ama gerçek HTTP konuşan servis)

  1. provideHttpClient()'ı kaydet. KullaniciServisi yaz: get<KullaniciDto[]>("https://jsonplaceholder.typicode.com/users"); DTO'da yalnız kullandığın alanları tiple ve tek dönüştürücüyle iç modele çevir.
  2. Tembellik deneyi: servisi çağır ama abone olma — Network sekmesinin boş kaldığını gör. Sonra şablona | async bağla, isteğin o an atıldığını izle.
  3. Üçlü durumu kur: startWith + map + catchError ile ListeDurumu akışı; @switch ile üç ekran çiz. URL'yi bilerek boz (404) ve hata dalının çizildiğini doğrula.
  4. Çift istek deneyi: aynı akışa şablonda iki ayrı | async bağla; Network'te iki isteği gör. Önce as ile tek bağlamaya, sonra shareReplay(1)'e çevirip ikisinin de çözdüğünü doğrula.
  5. İki interceptor yaz ve sırayla kaydet: süre ölçen (finalize) ve her isteğe X-Deneme: 1 başlığı ekleyen (clone). Network'te başlığı, konsolda süreyi gör; kayıt sırasını değiştirip günlük çıktısının nasıl değiştiğine bak.
  6. İptal deneyi: DevTools'ta ağı "Slow 3G" yap, istek sürerken bileşeni @if ile yok et — Network'te isteğin canceled olduğunu gör. async pipe'ın bunu senin yerine yaptığını bir cümleyle açıkla.

09Kendini sına

HttpClient'ın fetch'e göre farkları nelerdir? "İstek gitmiyor" ve "istek iki kez gitti" şikâyetlerinin sebebi nedir?

HttpClient, fetch'in üstüne dört şey koyar: dönüşler Observable'dır — RxJS operatörleriyle (debounce, switchMap, retry) birleşir ve abonelik bırakılınca istek gerçekten iptal edilir; JSON ayrıştırma ve hata nesneleştirme yerleşiktir — fetch'in "404'te reject etmez" tuzağı yoktur, başarısız durum kodu doğrudan error kanalına HttpErrorResponse düşürür; tip parametresi alır; ve interceptor zinciri sayesinde kimlik/günlük/hata işleri tek noktadan yapılır. İki klasik şikâyetin kökü aynıdır: Observable tembel ve soğuktur. Tembel: abone olunmadan istek atılmaz — "servisi çağırdım, Network boş" durumunda neredeyse her zaman subscribe/async pipe eksiktir; özellikle POST/DELETE çağrılarında unutulur. Soğuk: her abonelik isteği baştan atar — şablonda aynı akışa iki | async bağlamak Network'te iki istek demektir. Çözümler: @if (x$ | async; as x) ile tek bağlama ya da akışı shareReplay(1) ile paylaşıma açma; ikincisinde bayatlama sorumluluğu alındığını da söylemek gerekir.

get<T> yazınca veri tip güvenli olur mu? Sınırda hangi disiplin uygulanır?

Olmaz — bu, mülakatların sevdiği bir dürüstlük sorusudur. Tip parametresi derleme anı beyanıdır; tipler çalışma anında silindiği için (9. hat) HttpClient gelen JSON'u hiçbir şeyle karşılaştırmaz — get<T> aslında gizli bir as T'dir. Sunucu bir alanın adını değiştirirse kod derlenmeye devam eder ve ekran sessizce undefined gösterir. Disiplin 9. hattın sınır ilkesidir: DTO gelen hâliyle dürüst tiplenir — tarih alanları string'dir çünkü JSON'da tarih tipi yoktur; DTO'dan iç modele geçiş tek dönüştürücü fonksiyondan yapılır (tarih çevirisi, alan eşlemesi orada) — sunucu değişince tek dosya değişir; kritik sınırlarda dönüşümün önüne çalışma anı koruyucuları konur. Sıradan ekranlarda beyan + tek dönüştürücüyle yetinmek yaygın ve savunulabilir bir risk kararıdır — önemli olan bunun bir karar olduğunu bilmektir, "tip yazdım, güvendeyim" yanılgısı değil.

Interceptor nedir; fonksiyonel interceptor nasıl yazılır? clone ve kayıt sırası neden önemlidir?

Interceptor, uygulamadaki her HTTP isteğinin ve cevabının geçtiği ara katmandır — ASP.NET Core middleware boru hattının istemci karşılığı: istek, kayıt dizisindeki sırayla katmanlardan aşağı iner, cevap aynı zincirden yukarı çıkar. Modern yazım fonksiyoneldir: HttpInterceptorFn tipinde bir fonksiyon (req, next) alır, işini yapıp next(req) döndürür; guard'lar gibi injection context içinde çalıştığından gövdesinde inject() ile servis alabilir — sınıfsız, kurucusuz ara katman. Tipik işler: Authorization başlığı ekleme, süre/günlük tutma, genel hata yakalama. clone zorunludur çünkü istek nesnesi değişmezdir — başlık eklemek req.clone({ setHeaders: ... }) ile kopya üretmektir; bu değişmezlik, aynı isteğin güvenle tekrar denenebilmesinin (retry) temelidir. Kayıt sırası çalışma sırasıdır: süre ölçen interceptor kimlik ekleyenden önce kayıtlıysa onun maliyetini de ölçer; günlük en dışta isteniyorsa dizide en başa yazılır — middleware'de sıralama neden tasarım kararıysa burada da odur.

HTTP hata yönetimini uçtan uca anlat: HttpErrorResponse, catchError'ın yeri, retry'ın sınırı.

Başarısız istek error kanalına HttpErrorResponse düşürür; ilk iş status okumaktır: 0 cevabın hiç gelmediğini söyler (ağ kesik, sunucu kapalı ya da CORS — istemciden çözülmez), 400/422 doğrulama hatasıdır ve sözleşmedeki alan hataları forma geri yazılır, 401/403 kimlik/yetki işidir, 5xx sunucunun sorunudur — kullanıcıya genel mesaj verilir, ayrıntı dökülmez. Gövdedeki hata sözleşmesi (Problem Details) tek bir çeviri fonksiyonuyla kullanıcı diline çevrilir; ham err.message ekrana basılmaz. catchError'ın yeri akışın ömrüne bağlıdır: sürekli yaşayacak akışlarda (arama kutusu) iç akışa konur — dışa konursa ilk hata dış akışı öldürür ve arama tümden susar; tek seferlik ekran yüklemesinde zincir sonunda hatayı { tur: "hata" } gibi bir duruma çevirmek yeterlidir, yükleniyor/hata/veri üçlüsü tek Observable'da modellenir. retry yalnız okumalarda kullanılır: GET idempotenttir; POST'u tekrarlamak çift kayıt riskidir. Ayrıca 4xx'i tekrarlamak anlamsızdır — istek bozuktur; tekrar, ağ hatası (0) ve 5xx ile sınırlanıp gecikmeyle yapılır.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Angular'da HTTP isteği nasıl atılır; provideHttpClient nedir? (2) HttpClient'ın fetch'ten farkları nelerdir? (3) İstek neden abone olunmadan gitmez; bunun pratik sonucu nedir? (4) get<T> tip güvenliği sağlar mı; sınırda ne yapılır? (5) Yükleniyor/hata/veri durumları nasıl modellenir? (6) HttpErrorResponse'ta status: 0 ne anlama gelir? (7) Interceptor nedir; hangi işler için kullanılır? (8) İstek nesnesi neden clone edilir? (9) retry hangi isteklerde kullanılmaz, neden? (10) shareReplay(1) ne çözer, ne bedel getirir?

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

Düzen: (1) Tanım — "HttpClient, Observable döndüren tipli HTTP istemcisidir; kökte provideHttpClient ile kaydedilir, bileşen değil servis katmanı kullanır." (2) Akış doğası — "Tembel ve soğuktur: abonesiz istek gitmez, çift abonelik çift istektir; async pipe hem abone olur hem bileşenle birlikte isteği iptal eder." (3) Sınır — "get<T> beyandır, kanıt değil: DTO'yu geldiği gibi tipler, tek dönüştürücüden geçiririm — 9. hattın sınırda doğrula ilkesi." (4) Hata — "status'u okurum: 0 ağ/CORS, 4xx istemci, 5xx sunucu; catchError'ı yaşayacak akışta iç akışa koyarım; retry yalnız okumada." (5) Kıdem vuruşu — "Kesişen işleri fonksiyonel interceptor'da toplarım: inject ile bağımlılık alır, clone ile değişmez isteği kopyalar; sıra middleware gibi tasarım kararıdır; kimlik başlığı tek kapıdan çıkar."