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
provideHttpClientkaydı veget<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
HttpErrorResponseanatomisi;catchError'ın yeri;retry'ın sınırı- Fonksiyonel interceptor: kimlik başlığı, günlükleme, sıra ve
clonekuralı
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ıpsubscribe(ya daasyncpipe) bağlamazsan istek ağa hiç çıkmaz. "Servisi çağırdım ama Network sekmesi boş" şikâyetinin cevabı neredeyse her zaman budur. Özelliklepost/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
| asyncbağlarsan Network'te iki istek görürsün — 5. derste verilen sözün kanıtı. Çözüm oradan:asile 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,Datedeğ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 oradaDate'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:
status | Anlamı | Ekrana / koda düşen |
|---|---|---|
| 0 | Cevap 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 / 403 | Kimlik yok / yetki yok | Girişe yönlendir / "yetkin yok" — ayrıntısı 13. hatta |
| 404 | Kaynak yok | "Bulunamadı" ekranı; listeye dönüş bağlantısı |
| 5xx | Sunucu düştü — senin isteğin suçsuz olabilir | Genel 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 koddaHttpInterceptorarayüzünü uygulayan sınıflar okuyacaksın; fikir aynıdır.) clonezorunluluğ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
InjectionTokenkalı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. hattaAbortControllerile elle yaptığın işin tamamı, operatör seçiminin yan ürünü oldu. - Bileşen
HttpClientenjekte etmez: bileşendeinject(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)
provideHttpClient()'ı kaydet.KullaniciServisiyaz: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.- Tembellik deneyi: servisi çağır ama abone olma — Network sekmesinin boş
kaldığını gör. Sonra şablona
| asyncbağla, isteğin o an atıldığını izle. - Üçlü durumu kur:
startWith+map+catchErrorileListeDurumuakışı;@switchile üç ekran çiz. URL'yi bilerek boz (404) ve hata dalının çizildiğini doğrula. - Çift istek deneyi: aynı akışa şablonda iki ayrı
| asyncbağla; Network'te iki isteği gör. Önceasile tek bağlamaya, sonrashareReplay(1)'e çevirip ikisinin de çözdüğünü doğrula. - İki interceptor yaz ve sırayla kaydet: süre ölçen (
finalize) ve her isteğeX-Deneme: 1baş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. - İptal deneyi: DevTools'ta ağı "Slow 3G" yap, istek sürerken bileşeni
@ifile yok et — Network'te isteğin canceled olduğunu gör.asyncpipe'ı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."
