Ders 12 / 22
REST İstemcisi
Uygulamanın sunucuyla konuşan tek kapısı olarak istek katmanı; hata sözleşmesinin dört sınıfı, durum kodundan hataya eşleme, iptal ve zaman aşımı ile kaynak adreslerinin tasarımı.
İçindekiler
Durum yönetimi konusu, uygulamanın hangi bilgiyi nerede tuttuğunu ve bu bilginin sayfa kapandıktan sonra nasıl hayatta kaldığını kurdu. Kap hazır: yerel durum bileşenin içinde, paylaşılan durum ortak bir depoda, adreste taşınan durum rotada, kalıcı olması gerekenler depolamada duruyor. Kabın en büyük bölümü ise henüz boş — sunucu durumu.
Kuzey Yamaç Ölçüm İstasyonu arayüzünde istasyon listesi, her istasyonun ölçüm geçmişi ve yeni ölçüm kaydı sunucuda tutulur. Bu ders o veriyi almanın hangi sözleşmeyle yapıldığını ele alır: isteği kim kurar, yanıt nasıl çözülür, işler yolunda gitmediğinde görünüme ne bildirilir. Sorunun cevabı bir işlev değil, bir kattır.
İstek Katmanı Neden Ayrı Bir Kat
Veriyi çeken kod doğrudan bileşenin içine yazıldığında dört şey her çağrı noktasında tekrarlanır: taban adresin birleştirilmesi, ortak başlıkların eklenmesi, yanıtın çözümlenmesi ve hatanın yorumlanması. Tekrar yalnız yazım maliyeti değildir; her tekrar ayrı bir davranış üretir. Bir yerde 404 boş liste sayılır, başka yerde hata sayılır; bir yerde zaman aşımı vardır, başka yerde yoktur.
İstek katmanı, bu dört sorumluluğu tek yere toplayan ince bir aradır. Görünüme iki şey söz verir: her çağrı ya veri döndürür ya da hata sözleşmesine uyan bir hata nesnesi döndürür; üçüncü bir olasılık yoktur. Katmanın altında HTTP vardır, üstünde uygulama kavramları. İstasyon listesini isteyen kod durum kodu bilmez, yalnız “bulunamadı” ya da “yetki yok” görür.
Katmanın sınırı da bellidir. İstek katmanı önbellek tutmaz, yeniden denemez, yükleme göstergesi yönetmez. Bunlar sırasıyla sunucu durumu yönetiminin, dayanıklılık düzeninin ve görünüm durum makinesinin işidir; sonraki dersler bunları ayrı ayrı kurar. Katmanın tek işi, ağın belirsizliğini kapalı ve sayılabilir bir sonuç kümesine çevirmektir.
Hata Sözleşmesi
Bir istek dört ayrı yerde başarısız olabilir ve bu dördü aynı şey değildir.
- Ağ hatası. İstek karşı tarafa hiç ulaşmadı: ad çözümlenemedi, bağlantı kurulamadı, bağlantı koptu. Sunucu bu isteği görmemiş olabilir — ama görmüş de olabilir; yanıtın yolda kaybolması da aynı hataya benzer. Bu belirsizlik, yeniden deneme kararında belirleyici olacak.
- HTTP hatası. İstek ulaştı, sunucu yanıt verdi, yanıt başarısızlık bildiriyor. Sunucunun ne düşündüğü durum kodunda yazılıdır ve bu bilgi güvenilirdir.
- Gövde hatası. Yanıt geldi, durum kodu başarı diyor, ama gövde beklenen biçimde değil: bozuk JSON, eksik alan, farklı içerik türü. Araya giren bir vekil sunucunun HTML hata sayfası döndürmesi bu sınıfa girer.
- İptal. İstek, sonucu artık kimse beklemediği için durduruldu. Kullanıcı sayfadan ayrıldı, arama kutusuna yeni bir harf yazdı ya da zaman aşımı doldu. Bu bir başarısızlık değil, bir karardır; kullanıcıya hata gösterilmez.
Dördünün ortak bir gösterimi olmalı, yoksa her çağrı noktası kendi ayrımını yeniden
uydurur. Sözleşme şu alanlardan oluşur: tur (dört sınıftan biri), kod (uygulamanın
tanıdığı sabit ad), durum (varsa HTTP durum kodu), yenidenDenenebilir (mantıksal) ve
ayrinti (sunucunun gönderdiği yapılandırılmış açıklama). İleti metni sözleşmede tutulmaz
— hangi dilde ve hangi tonda yazılacağı görünümün kararıdır; bu ayrımı Uluslararasılaştırma
dersi kapatır.
Durum Kodundan Hataya Eşleme
Eşleme bir tablo işidir ve tablo tek yerde durur. İnternet Nasıl Çalışır kursundaki HTTP
İstek ve Yanıtı dersinde tanıtılan durum kodu sınıfları burada uygulama kararına çevrilir:
4xx isteğin kendisinde bir sorun olduğunu söyler, aynı isteği tekrarlamak sonucu
değiştirmez; 5xx sunucunun o an tamamlayamadığını söyler, tekrarlamak işe yarayabilir.
// durum-eslemesi.mjs — HTTP durum kodundan hata sozlesmesine gecis const ESLEME = [ [400, "istek_bicimi", false, "İstek gövdesi okunamadı"], [401, "kimlik_yok", false, "Oturum gerekiyor"], [403, "yetki_yok", false, "Bu kayda erişim yok"], [404, "bulunamadi", false, "Kayıt yok"], [409, "cakisma", false, "Kayıt bu arada değişti"], [422, "dogrulama", false, "Alan değerleri kabul edilmedi"], [429, "hiz_siniri", true, "İstek hızı aşıldı"], [500, "sunucu", true, "Sunucu isteği tamamlayamadı"], [503, "erisilemez", true, "Hizmet geçici olarak kapalı"], ]; function httpHatasi(durum, ayrinti = null) { const satir = ESLEME.find(([d]) => d === durum); if (satir) { const [, kod, yenidenDenenebilir, ileti] = satir; return { tur: "http", durum, kod, ileti, yenidenDenenebilir, ayrinti }; } // Bilinmeyen kod: sinifina gore karar verilir, uydurma kod uretilmez. const sunucuSinifi = durum >= 500; return { tur: "http", durum, kod: sunucuSinifi ? "sunucu" : "istek", ileti: sunucuSinifi ? "Sunucu isteği tamamlayamadı" : "İstek kabul edilmedi", yenidenDenenebilir: sunucuSinifi, ayrinti, }; } for (const durum of [401, 404, 422, 429, 500, 418, 504]) { const h = httpHatasi(durum); console.log( String(h.durum).padEnd(4), h.kod.padEnd(14), "yeniden:", String(h.yenidenDenenebilir).padEnd(5), h.ileti, ); }
401 kimlik_yok yeniden: false Oturum gerekiyor 404 bulunamadi yeniden: false Kayıt yok 422 dogrulama yeniden: false Alan değerleri kabul edilmedi 429 hiz_siniri yeniden: true İstek hızı aşıldı 500 sunucu yeniden: true Sunucu isteği tamamlayamadı 418 istek yeniden: false İstek kabul edilmedi 504 sunucu yeniden: true Sunucu isteği tamamlayamadı
Son iki satır tablonun asıl değerini gösterir. Tabloda olmayan kodlar da bir karara bağlanır ve karar sınıfın anlamından türer; katman “bilinmeyen kod” diye çöküp görünümü tanımsız bir duruma bırakmaz. 429 ilginç bir istisnadır: 4xx sınıfındadır ama yeniden denenebilir, çünkü sorun isteğin içeriğinde değil zamanlamasındadır.
Katmanın Uçtan Uca Çalışması
Aşağıdaki dosya hem yerel bir sunucu kurar hem de istek katmanını o sunucuya karşı çalıştırır; kendi başına çalışır ve sonunda sunucuyu kapatır. Bağlantı noktası sıfır verildiği için işletim sistemi boş bir kapı seçer — o sayı çalıştıran makineye göre değişir ve çıktıda görünmez.
// istek-katmani.mjs — yerel sunucu + istek katmani, tek dosyada calisir import http from "node:http"; // --- Sunucu: istasyon listesi, olcum gecmisi ve hata yollari ---------------- const ISTASYONLAR = [ { kod: "KY-01", ad: "Kuzey Yamaç", yukseklik: 1840 }, { kod: "KY-02", ad: "Kuzey Yamaç Sırt", yukseklik: 2110 }, ]; const sunucu = http.createServer((istek, yanit) => { const adres = new URL(istek.url, "http://127.0.0.1"); const gonder = (durum, govde, tur = "application/json") => { yanit.writeHead(durum, { "content-type": tur }); yanit.end(typeof govde === "string" ? govde : JSON.stringify(govde)); }; if (adres.pathname === "/istasyonlar") return gonder(200, ISTASYONLAR); if (adres.pathname === "/istasyonlar/KY-99") return gonder(404, { kod: "istasyon_bulunamadi", ileti: "KY-99 kayıtlı değil" }); if (adres.pathname === "/olcumler" && istek.method === "POST") return gonder(422, { kod: "dogrulama", alanlar: { deger: "aralik_disi" } }); if (adres.pathname === "/rapor") return gonder(500, "<html>sunucu hatası</html>", "text/html"); if (adres.pathname === "/yavas") return setTimeout(() => gonder(200, { hazir: true }), 300); return gonder(404, { kod: "yol_yok", ileti: "Bilinmeyen yol" }); }); await new Promise((coz) => sunucu.listen(0, "127.0.0.1", coz)); const TABAN = `http://127.0.0.1:${sunucu.address().port}`; // --- Hata sozlesmesi -------------------------------------------------------- const ESLEME = new Map([ [401, ["kimlik_yok", false]], [403, ["yetki_yok", false]], [404, ["bulunamadi", false]], [422, ["dogrulama", false]], [429, ["hiz_siniri", true]], [500, ["sunucu", true]], [503, ["erisilemez", true]], ]); const hata = (tur, kod, yenidenDenenebilir, durum = null, ayrinti = null) => ({ tur, kod, yenidenDenenebilir, durum, ayrinti }); // --- Istek katmani ---------------------------------------------------------- async function istek(yol, secenekler = {}) { const { yontem = "GET", govde = null, zamanAsimi = 1000, taban = TABAN } = secenekler; const denetleyici = new AbortController(); const sayac = setTimeout(() => denetleyici.abort("zaman_asimi"), zamanAsimi); let yanit; try { yanit = await fetch(taban + yol, { method: yontem, headers: govde ? { "content-type": "application/json" } : {}, body: govde ? JSON.stringify(govde) : undefined, signal: denetleyici.signal, }); } catch { // Alt katmanin iletisi ortama gore degisir; disariya kendi kodumuz cikar. return { ok: false, hata: denetleyici.signal.aborted ? hata("iptal", String(denetleyici.signal.reason), false) : hata("ag", "baglanti", true) }; } finally { clearTimeout(sayac); } const tur = yanit.headers.get("content-type") ?? ""; let cozulen = null; if (tur.startsWith("application/json")) { try { cozulen = await yanit.json(); } catch { return { ok: false, hata: hata("govde", "cozumlenemedi", false, yanit.status) }; } } else { await yanit.text(); } if (!yanit.ok) { const [kod, yeniden] = ESLEME.get(yanit.status) ?? [ yanit.status >= 500 ? "sunucu" : "istek", yanit.status >= 500]; return { ok: false, hata: hata("http", kod, yeniden, yanit.status, cozulen) }; } return { ok: true, veri: cozulen }; } // --- Cagri noktalari -------------------------------------------------------- const yaz = (etiket, s) => console.log( etiket.padEnd(22), s.ok ? `ok ${JSON.stringify(s.veri)}` : `hata tur=${s.hata.tur} kod=${s.hata.kod} durum=${s.hata.durum} ` + `yeniden=${s.hata.yenidenDenenebilir}`); yaz("istasyon listesi", await istek("/istasyonlar")); yaz("bilinmeyen istasyon", await istek("/istasyonlar/KY-99")); yaz("ölçüm gönderimi", await istek("/olcumler", { yontem: "POST", govde: { deger: 900 } })); yaz("rapor (JSON değil)", await istek("/rapor")); yaz("zaman aşımı", await istek("/yavas", { zamanAsimi: 50 })); yaz("aynı yol, bol süre", await istek("/yavas", { zamanAsimi: 1000 })); // Kapali bir baglanti noktasi: ag hatasi yolu. const bos = http.createServer(); await new Promise((coz) => bos.listen(0, "127.0.0.1", coz)); const kapaliTaban = `http://127.0.0.1:${bos.address().port}`; await new Promise((coz) => bos.close(coz)); yaz("sunucu yanıt vermiyor", await istek("/istasyonlar", { taban: kapaliTaban })); sunucu.close();
istasyon listesi ok [{"kod":"KY-01","ad":"Kuzey Yamaç","yukseklik":1840},{"kod":"KY-02","ad":"Kuzey Yamaç Sırt","yukseklik":2110}]
bilinmeyen istasyon hata tur=http kod=bulunamadi durum=404 yeniden=false
ölçüm gönderimi hata tur=http kod=dogrulama durum=422 yeniden=false
rapor (JSON değil) hata tur=http kod=sunucu durum=500 yeniden=true
zaman aşımı hata tur=iptal kod=zaman_asimi durum=null yeniden=false
aynı yol, bol süre ok {"hazir":true}
sunucu yanıt vermiyor hata tur=ag kod=baglanti durum=null yeniden=true
Yedi satır, dört hata sınıfının hepsini gösteriyor. Dikkat edilecek üç nokta var.
Birincisi, yanıtın gövdesi her durumda tüketilir. Başarısız yanıtın gövdesi okunmadan bırakılırsa bağlantı serbest kalmaz; içerik türü JSON değilse bile gövde metin olarak okunup atılır. Beşinci satırdaki 500 yanıtı HTML döndürüyor — katman bunu çözmeye çalışmıyor, durum kodundan karar veriyor.
İkincisi, 422 yanıtının gövdesi ayrinti alanına taşınır. Alan bazlı doğrulama hatalarını
forma geri bağlayacak olan Doğrulama Şemaları dersi bu alanı kullanacak; istek katmanı
içeriği yorumlamaz, yalnızca taşır.
Üçüncüsü, iptal ile ağ hatası aynı catch bloğuna düşer ama ayrı sınıflandırılır. Ayrımı
yapan şey yakalanan değer değil, iptal işaretinin durumudur. İptal nedeni işaretin
üzerinde taşındığı için “kim iptal etti” sorusu da yanıtlanabilir; burada neden
zaman_asimi dizgisidir.
İptal, Zaman Aşımı ve Yarış
Eşzamansız JavaScript ve Çalışma Zamanı kursunda tanıtılan iptal işareti (abort signal) bu katmanın belkemiğidir. İki ayrı ihtiyaca aynı düzenek hizmet eder: bir süre sonra vazgeçmek ve sonucu artık istemediğimizde vazgeçmek.
Zaman aşımı, sunucunun yanıt vermemesi durumunda isteği sonsuza kadar açık tutmamayı sağlar. Süresiz bir istek, kullanıcının bakış açısından hiç bitmeyen bir yükleme göstergesidir. Sürenin ne olacağı işin niteliğine bağlıdır; istasyon listesi için kısa, büyük bir rapor için uzun olur, bu yüzden katman süreyi çağrı başına alır.
İkinci ihtiyaç daha incedir. İstasyon listesinde bir süzgeç alanı varsa kullanıcı yazdıkça istek üretilir. Üçüncü isteğin yanıtı ikincisinden önce gelirse ekranda eski sonuç kalır. Çözümün bir yarısı, yeni istek başlarken öncekini iptal etmektir; katmanın dışarıdan bir iptal işareti kabul etmesi bunu mümkün kılar. Diğer yarısı — geç gelen yanıtın yok sayılması — görünüm tarafındadır ve Yükleme ve Hata Durumları dersinde ele alınır.
İptal edilen istek görünüme hata olarak yansımaz. Katmanın iptal sınıfını ayrı tutmasının
tek sebebi budur: çağrı noktası bu sonucu görünce hiçbir şey yapmaz, çünkü ekranı zaten
yeni bir istek dolduracaktır.
Kaynak Adresleri ve Sözleşmenin Sınırı
REST’in katman açısından anlamı sadedir: her kaynağın bir adresi vardır ve HTTP yöntemi ne
yapılacağını söyler. Koleksiyon /istasyonlar, tekil kayıt /istasyonlar/KY-01, bağlı
koleksiyon /istasyonlar/KY-01/olcumler olur. Süzme, sıralama ve sayfalama ölçütleri
adresin sorgu bölümünde taşınır; bunlar kaynağın kimliğini değil görünümünü belirler.
Bu düzenin istemci tarafında iki doğrudan sonucu vardır. Birincisi, adres bir önbellek anahtarıdır: aynı adrese yapılan iki istek aynı kaydı ister ve sonucu paylaşabilir. İkincisi, yöntem yeniden denenebilirliği belirler. İnternet Nasıl Çalışır kursunda tanımlanan güvenli ve değişmez yöntem ayrımı burada iş görür: bir okuma isteğini tekrarlamak zararsızdır, bir ölçüm kaydını iki kez göndermek iki kayıt üretebilir.
Sözleşmenin sınırı da bu noktada belirir. İstemci, sunucunun kaynakları hangi ayrıntı düzeyinde döndürdüğüne uymak zorundadır. Ölçüm geçmişi ekranı istasyon adını, son ölçümü ve ölçüm listesini birlikte gösteriyorsa üç ayrı istek gerekebilir; tek bir ekran için üç gidiş dönüş, gecikmenin doğrudan çarpanıdır. Sunucu bunu önlemek için ekrana göre birleşik yanıtlar tasarlarsa bu kez kaynak modeli ekrana bağlanır ve her yeni ekran yeni bir uç nokta ister.
Özet
- İstek katmanı taban adres, ortak başlıklar, gövde çözümleme ve hata yorumlama sorumluluklarını görünümden alıp tek yere toplar; önbellek, yeniden deneme ve yükleme göstergesi onun işi değildir.
- Hata sözleşmesi dört sınıf tanır: ağ, HTTP, gövde ve iptal. Ağ hatasında isteğin sunucuya ulaşıp ulaşmadığı belirsizdir; HTTP hatasında sunucunun kararı bilinir.
- Durum kodu eşlemesi tek bir tabloda tutulur ve tabloda olmayan kodlar sınıflarının anlamına göre karara bağlanır; 429, 4xx sınıfında olmasına rağmen yeniden denenebilirdir.
- Başarısız yanıtın gövdesi de tüketilir; sunucunun gönderdiği yapılandırılmış açıklama
yorumlanmadan
ayrintialanında taşınır. - İptal işareti hem zaman aşımını hem de vazgeçmeyi karşılar; iptal edilen istek görünüme hata olarak yansımaz.
- Kaynak adresi aynı zamanda önbellek anahtarıdır; HTTP yönteminin güvenli ve değişmez olması yeniden denenebilirliği belirler.
Sonraki Adım
Bu dersin son bölümü bir gerilim bıraktı: ekranın istediği veri ile kaynağın döndürdüğü veri aynı biçimde değil. Ölçüm geçmişi ekranı için üç istek atmak da, ekrana özel bir uç nokta açmak da bedelli. Sorunun kökü, ne isteneceğine sunucunun karar vermesi. Sonraki ders bu kararı istemciye veren bir sorgu diline bakar: istemci hangi alanları istediğini yazar, tek istekte alır. Bunun karşılığında iki yeni sorun doğar — gelen iç içe yanıtın önbellekte nasıl saklanacağı ve aynı alan kümesinin ekranlar arasında nasıl paylaşılacağı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.