Ders 18 / 34
Hata Yanıt Biçimi
Hata gövdesinin uç nokta başına uydurulmasının istemciye maliyeti, standart problem ayrıntısı biçiminin beş çekirdek alanı ve gövdeyi tek katmandan üreten sorun kataloğunun ölçülerek doğrulanması.
İçindekiler
Bağlantı odaklı yanıtlar dersi, başarılı yanıtın içine gezinme bilgisini koydu: istemci bir ödünç kaydını aldığında, o kayıttan iade işlemine ve üyeye nasıl gidileceğini yanıtın kendisinden okuyabiliyordu. Kaynak modeli, adres düzeni, durum kodu seçimi, gövde adlandırması ve bağlantılar — buraya kadar tasarlanan her şey isteğin başarıyla sonuçlandığı durumu anlatıyordu.
Başarısız istekler bu tasarımın dışında kaldı. Ödünç servisinin gerçek trafiğinde kitabı bulunamayan, kopyası kalmayan, sınırı aşan, gövdesi bozuk istekler vardır. Bu ders tek bir soruyu yanıtlar: hata yanıtının gövdesi nasıl olmalıdır ki istemci onu uç nokta başına yeniden öğrenmek zorunda kalmasın?
İki Hata Biçemi, Tek Servis
Aşağıdaki sunucu ödünç servisinin bilinen çekirdeğini sunar — kitap sorgulama, ödünç verme,
iade — ve hata gövdesini iki ayrı biçemde üretebilir. Yönlendirme kodu ikisinde de aynıdır;
değişen yalnızca hata() çağrısının ürettiği gövdedir. DAGINIK tablosu, hata gövdesi
sözleşmeye yazılmadığında bir kod tabanında zamanla oluşan hâldir: her durum, o satırı
yazan kişinin aklına gelen adlarla bildirilir. PROBLEM tablosu ise her durumu bir sorun
türü kataloğuna eşler.
// sorun.mjs — problem ayrintisi katmani: govdeyi ureten tek yer export const TABAN_TIP = "https://ornek.kutuphane/sorunlar/"; // Katalog: her sorun turu bir kez tanimlanir. Tur -> kalici kimlik, baslik, durum kodu. export const KATALOG = { kaynak_yok: { baslik: "Kaynak bulunamadı", durum: 404 }, govde_bozuk: { baslik: "İstek gövdesi çözülemedi", durum: 400 }, kopya_yok: { baslik: "Ödünç verilebilir kopya yok", durum: 409 }, uye_siniri: { baslik: "Üye ödünç sınırına ulaştı", durum: 409 }, dogrulama: { baslik: "İstek gövdesi doğrulanamadı", durum: 422 }, }; let sayac = 0; const olayKimligi = () => `ol-${String(++sayac).padStart(4, "0")}`; // sorun(kod, ayrinti, ek) -> { govde, durum } export function sorun(kod, ayrinti, ek = {}) { const kayit = KATALOG[kod]; if (!kayit) throw new Error(`katalogda olmayan sorun türü: ${kod}`); return { durum: kayit.durum, govde: { type: TABAN_TIP + kod.replaceAll("_", "-"), title: kayit.baslik, status: kayit.durum, detail: ayrinti, instance: olayKimligi(), ...ek, }, }; }
// sunucu.mjs — odunc servisi; ayni yollar, iki ayri hata bicemi // Kullanim: node sunucu.mjs dagink (uc nokta basina uydurulmus govde) // node sunucu.mjs problem (problem ayrintisi bicimi) import { createServer } from "node:http"; import { sorun } from "./sorun.mjs"; const BICEM = process.argv[2] ?? "dagink"; // Dagink bicem: her hata durumu icin o gun yazilmis govde. const DAGINIK = { kitap_yok: (b) => [404, { error: "not_found", resource: "kitap", id: b.id }], odunc_yok: (b) => [404, { message: `odunc bulunamadi: ${b.id}` }], uye_yok: () => [404, { hata: { kod: 4041, aciklama: "uye yok" } }], govde_bozuk: () => [400, { ok: false, reason: "gecersiz JSON" }], kopya_yok: () => [409, { conflict: "kopya_yok", kalan: 0 }], uye_siniri: (b) => [409, { hataMesaji: "uye siniri asildi", limit: b.sinir }], yol_yok: (b) => [404, { status: 404, path: b.yol }], }; // Problem bicemi: her durum, katalogdaki bir sorun turune eslenir. const PROBLEM = { kitap_yok: (b) => sorun("kaynak_yok", `ISBN ${b.id} katalogda yok.`, { kaynak: "kitap" }), odunc_yok: (b) => sorun("kaynak_yok", `${b.id} numaralı ödünç kaydı yok.`, { kaynak: "odunc" }), uye_yok: (b) => sorun("kaynak_yok", `${b.id} numaralı üye yok.`, { kaynak: "uye" }), govde_bozuk: () => sorun("govde_bozuk", "Gövde geçerli JSON değil.", { kaynak: "govde" }), kopya_yok: (b) => sorun("kopya_yok", `${b.ad} kitabının kopyaları ödünçte.`, { isbn: b.id, kalan: 0 }), uye_siniri: (b) => sorun("uye_siniri", `Üye ${b.acik} kitap tutuyor, sınır ${b.sinir}.`, { sinir: b.sinir, acik: b.acik }), yol_yok: (b) => sorun("kaynak_yok", `${b.yol} yolu tanımlı değil.`, { kaynak: "yol" }), }; const hata = (yanit, kod, baglam = {}) => { if (BICEM === "problem") { const { durum, govde } = PROBLEM[kod](baglam); yanit.writeHead(durum, { "content-type": "application/problem+json; charset=utf-8" }); return yanit.end(JSON.stringify(govde)); } const [durum, govde] = DAGINIK[kod](baglam); yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8" }); yanit.end(JSON.stringify(govde)); }; const KITAPLAR = new Map([ ["978-0201896831", { ad: "Programlama Sanati", kopya: 2 }], ["978-0262033848", { ad: "Algoritmalara Giris", kopya: 1 }], ]); const UYELER = new Set(["U-1001", "U-1002"]); const UYE_SINIRI = 2; const oduncler = [{ id: "O-1", uye: "U-1001", isbn: "978-0262033848" }]; const govdeOku = (istek) => new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); }); const json = (yanit, kod, nesne) => { yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" }); yanit.end(JSON.stringify(nesne)); }; createServer(async (istek, yanit) => { yanit.sendDate = false; const yol = istek.url.split("?")[0]; if (istek.method === "GET" && yol.startsWith("/kitaplar/")) { const isbn = yol.slice("/kitaplar/".length); const kitap = KITAPLAR.get(isbn); if (!kitap) return hata(yanit, "kitap_yok", { id: isbn }); return json(yanit, 200, { isbn, ...kitap }); } if (istek.method === "GET" && yol.startsWith("/odunc/")) { const id = yol.slice("/odunc/".length); const kayit = oduncler.find((o) => o.id === id); if (!kayit) return hata(yanit, "odunc_yok", { id }); return json(yanit, 200, kayit); } if (istek.method === "POST" && yol === "/odunc") { let govde; try { govde = JSON.parse((await govdeOku(istek)) || "{}"); } catch { return hata(yanit, "govde_bozuk"); } if (!UYELER.has(govde.uye)) return hata(yanit, "uye_yok", { id: govde.uye }); const kitap = KITAPLAR.get(govde.isbn); if (!kitap) return hata(yanit, "kitap_yok", { id: govde.isbn }); const elde = oduncler.filter((o) => o.isbn === govde.isbn).length; if (elde >= kitap.kopya) return hata(yanit, "kopya_yok", { id: govde.isbn, ad: kitap.ad }); const acik = oduncler.filter((o) => o.uye === govde.uye).length; if (acik >= UYE_SINIRI) return hata(yanit, "uye_siniri", { sinir: UYE_SINIRI, acik }); const id = `O-${oduncler.length + 1}`; oduncler.push({ id, uye: govde.uye, isbn: govde.isbn }); return json(yanit, 201, { id, uye: govde.uye, isbn: govde.isbn }); } if (istek.method === "POST" && yol === "/iade") { const govde = JSON.parse((await govdeOku(istek)) || "{}"); const sira = oduncler.findIndex((o) => o.id === govde.oduncId); if (sira < 0) return hata(yanit, "odunc_yok", { id: govde.oduncId }); oduncler.splice(sira, 1); return json(yanit, 200, { durum: "iade edildi" }); } hata(yanit, "yol_yok", { yol }); }).listen(8431, "127.0.0.1", () => console.log(`sunucu 127.0.0.1:8431 biçem=${BICEM}`));
Problem Ayrıntısı Biçimi
PROBLEM tablosunun ürettiği yapı uydurulmuş değildir: hata gövdesi için standartlaşmış
bir biçim vardır. Problem ayrıntısı (problem details) RFC 9457 ile tanımlanır,
application/problem+json içerik tipiyle taşınır ve beş çekirdek alanı bulunur.
type— sorun türünün kalıcı kimliği, bir URI. İstemcinin dallandığı alan budur. Metin değil kimliktir; başlık değişse bile aynı kalır.title— sorun türünün insan okur kısa adı. Aynıtypeiçin her zaman aynıdır.status— HTTP durum kodunun gövdedeki kopyası.detail— bu örneğe özgü açıklama: hangi ISBN, hangi üye, hangi sınır.instance— bu tek olayın kimliği; kütükteki kaydı bulmayı sağlar.
type ile detail arasındaki ayrım biçimin çekirdeğidir. type sınıfı adlandırır,
detail örneği anlatır. İstemci type alanına bakarak karar verir (“kopya yoksa
rezervasyon düğmesini göster”), detail alanını yalnızca kullanıcıya gösterir ya da kütüğe
yazar. Bu ayrım korunmazsa istemci metin karşılaştırmasına mecbur kalır ve sunucudaki bir
yazım düzeltmesi istemci mantığını bozar.
status alanının gövdede yinelenmesi ilk bakışta gereksiz görünür — durum satırında zaten
vardır. İki durumda gerekli olur: yanıt bir kütüğe ya da kuyruğa gövde olarak alındığında
durum satırı kaybolur; bir ara katman durum kodunu değiştirdiğinde gövdedeki değerle durum
satırı arasındaki uyuşmazlık bu değişikliği görünür kılar.
Standart, çekirdeğin dışına ek üye (extension member) koymaya izin verir. Kalan kopya
sayısı, sınır değeri, hangi kaynağın aranıp bulunamadığı — bunlar ek üyedir ve sorun
türüne bağlıdır: aynı type her zaman aynı ek üyelerle gelir.
Ölçüm
Aşağıdaki betik sunucudaki sekiz hata durumunu sırayla üretir, gövdeleri toplar ve üç şey sayar: kaç ayrı içerik tipi döndüğünü, kaç ayrı anahtar kümesi çıktığını ve çekirdek beş alanın kaç ayrı bileşimde göründüğünü.
// denetle.mjs — bir sunucunun hata yanitlarindaki bicim tutarliligini olcer // Kullanim: node denetle.mjs <taban-adres> const TABAN = process.argv[2]; const CEKIRDEK = ["type", "title", "status", "detail", "instance"]; // Her satir bir hata durumu uretir: [ad, yontem, yol, govde] const DURUMLAR = [ ["kitap yok (GET)", "GET", "/kitaplar/978-0000000000", null], ["odunc yok (GET)", "GET", "/odunc/O-99", null], ["bozuk govde", "POST", "/odunc", "{bu json degil"], ["uye yok", "POST", "/odunc", { uye: "U-9999", isbn: "978-0201896831" }], ["kitap yok (POST)", "POST", "/odunc", { uye: "U-1001", isbn: "978-0000000000" }], ["kopya yok", "POST", "/odunc", { uye: "U-1002", isbn: "978-0262033848" }], ["odunc yok (iade)", "POST", "/iade", { oduncId: "O-99" }], ["yol yok", "GET", "/rafler", null], ]; const anahtarlar = (n, onek = "") => Object.entries(n).flatMap(([a, d]) => d && typeof d === "object" && !Array.isArray(d) ? anahtarlar(d, `${onek}${a}.`) : [`${onek}${a}`]); const cekirdekBicimleri = new Set(); const tamBicimler = new Set(); const tipler = new Set(); console.log("durum içerik tipi çekirdek ek üyeler"); for (const [ad, yontem, yol, govde] of DURUMLAR) { const cevap = await fetch(TABAN + yol, { method: yontem, headers: govde == null ? {} : { "content-type": "application/json" }, body: govde == null ? undefined : typeof govde === "string" ? govde : JSON.stringify(govde), }); const metin = await cevap.text(); const tip = (cevap.headers.get("content-type") ?? "").split(";")[0]; tipler.add(tip); let liste; try { liste = anahtarlar(JSON.parse(metin)); } catch { liste = ["<ayrıştırılamadı>"]; } const cekirdek = CEKIRDEK.filter((a) => liste.includes(a)); const ek = liste.filter((a) => !CEKIRDEK.includes(a)).sort(); cekirdekBicimleri.add(cekirdek.join(",")); tamBicimler.add([...cekirdek, ...ek].join(",")); const durumu = cekirdek.length === CEKIRDEK.length ? "tam" : `eksik(${cekirdek.length}/5)`; console.log(`${ad.padEnd(20)} ${tip.padEnd(24)} ${durumu.padEnd(14)} ${ek.join(",") || "-"}`); } console.log(`\nsınanan hata durumu: ${DURUMLAR.length}`); console.log(`ayrı içerik tipi: ${tipler.size} (${[...tipler].join(", ")})`); console.log(`ayrı tam anahtar kümesi: ${tamBicimler.size}`); console.log(`ayrı çekirdek biçimi: ${cekirdekBicimleri.size}`);
#!/usr/bin/env bash # sunucu.mjs'yi iki bicemle sirayla baslatir ve ayni denetimi calistirir. for bicem in dagink problem; do node sunucu.mjs "$bicem" & s=$! sleep 0.5 node denetle.mjs http://127.0.0.1:8431 kill "$s"; wait "$s" 2>/dev/null echo done
sunucu 127.0.0.1:8431 biçem=dagink durum içerik tipi çekirdek ek üyeler kitap yok (GET) application/json eksik(0/5) error,id,resource odunc yok (GET) application/json eksik(0/5) message bozuk govde application/json eksik(0/5) ok,reason uye yok application/json eksik(0/5) hata.aciklama,hata.kod kitap yok (POST) application/json eksik(0/5) error,id,resource kopya yok application/json eksik(0/5) conflict,kalan odunc yok (iade) application/json eksik(0/5) message yol yok application/json eksik(1/5) path sınanan hata durumu: 8 ayrı içerik tipi: 1 (application/json) ayrı tam anahtar kümesi: 6 ayrı çekirdek biçimi: 2 sunucu 127.0.0.1:8431 biçem=problem durum içerik tipi çekirdek ek üyeler kitap yok (GET) application/problem+json tam kaynak odunc yok (GET) application/problem+json tam kaynak bozuk govde application/problem+json tam kaynak uye yok application/problem+json tam kaynak kitap yok (POST) application/problem+json tam kaynak kopya yok application/problem+json tam isbn,kalan odunc yok (iade) application/problem+json tam kaynak yol yok application/problem+json tam kaynak sınanan hata durumu: 8 ayrı içerik tipi: 1 (application/problem+json) ayrı tam anahtar kümesi: 2 ayrı çekirdek biçimi: 1
Dağınık biçemde sekiz hata durumu altı ayrı anahtar kümesi üretiyor ve hiçbirinde ortak bir
çekirdek yok. error, message, reason, hataMesaji, hata.aciklama — hepsi aynı
şeyi, “ne oldu” bilgisini taşır, her biri başka addadır. Uygulama Mimarisi kursunda kurulan
istek katmanı yanıtı bir hata sözleşmesine (error contract) eşlemek zorundadır ve bu
tabloyla eşleme yazılamaz. İstemci ya altı ayrı ayrıştırıcı yazar ya da tek bir alana
bakıp diğerlerini görmezden gelir; ikinci seçenek yaygındır ve kullanıcının ekranında “bir
hata oluştu” cümlesine dönüşür.
Problem biçeminde ayrı çekirdek biçim sayısı bire iner ve her satırda beş alanın tamamı
bulunur. Tam anahtar kümesi ikidir, çünkü ek üyeler farklıdır ve farklı olmaları gerekir:
kopya_yok sorununun kalan alanı vardır, kaynak_yok sorununun yoktur. Ama bu fark
sorun türüyle bağlıdır, uç noktayla değil. İstemci type alanını okuduğunda hangi ek
üyelerin geleceğini bilir; dağınık biçemde hangi uç noktaya gittiğini bilmek zorundaydı.
İçerik tipi satırı ikinci bir kazanç gösterir. Dağınık biçemde hata yanıtı ile başarı
yanıtı aynı içerik tipini taşır; aradaki ara katmanlar ve kütükleme araçları gövdeyi
ayrıştırmadan bunun bir hata olup olmadığını anlayamaz. application/problem+json bu ayrımı
gövdeye bakmadan verir.
Aynı Sorunun Farklı Uç Noktalardan Dönüşü
Tabloda kaynak_yok sorunu altı satırda görünüyor. İstemcinin buna güvenebilmesi için o
satırların type, title ve status alanları birebir aynı olmalıdır. Aşağıdaki betik iki
farklı uç noktadan aynı sorunu üretir ve ardından kataloğa yazılmamış bir tür ister.
#!/usr/bin/env bash # Ayni sorun turunu iki ayri uc noktadan uretir; katalog disi turu dener. node sunucu.mjs problem & sunucu=$! sleep 0.5 for yol in /kitaplar/978-0000000000 /odunc/O-99; do echo "--- $yol ---" curl -sS -D - -o /tmp/govde "http://127.0.0.1:8431$yol" | grep -i '^HTTP\|^content-type' cat /tmp/govde; echo done kill "$sunucu"; wait "$sunucu" 2>/dev/null echo "--- katalog disi bir sorun turu istenirse ---" node -e 'import("./sorun.mjs").then(({ sorun }) => { try { sorun("rafta_toz_var", "Uydurulmus bir hata."); } catch (h) { console.log(h.message); } });'
sunucu 127.0.0.1:8431 biçem=problem
--- /kitaplar/978-0000000000 ---
HTTP/1.1 404 Not Found
content-type: application/problem+json; charset=utf-8
{"type":"https://ornek.kutuphane/sorunlar/kaynak-yok","title":"Kaynak bulunamadı","status":404,"detail":"ISBN 978-0000000000 katalogda yok.","instance":"ol-0001","kaynak":"kitap"}
--- /odunc/O-99 ---
HTTP/1.1 404 Not Found
content-type: application/problem+json; charset=utf-8
{"type":"https://ornek.kutuphane/sorunlar/kaynak-yok","title":"Kaynak bulunamadı","status":404,"detail":"O-99 numaralı ödünç kaydı yok.","instance":"ol-0002","kaynak":"odunc"}
--- katalog disi bir sorun turu istenirse ---
katalogda olmayan sorun türü: rafta_toz_var
İlk üç alan birebir aynı, detail ve instance farklı. İstemcinin durum kodu eşlemesi
type alanına bakar ve iki uç nokta için tek dal yazar; kullanıcıya gösterilecek metin
detail alanından gelir; destek kaydına instance yazılır.
Son satır kataloğun ikinci işlevini gösteriyor. Katalog yalnızca type ile title ve
status eşlemesini tutmaz; kendisinde olmayan bir tür istendiğinde hata verir. Kataloğa
yazılmamış bir sorun sessizce yeni bir biçim üretemez. Biçimin zamanla yeniden dağılmasını
engelleyen şey, biçimin doğru tasarlanmış olması değil, yanlış kullanımın çalışma anında
görünür olmasıdır.
Gövdeye Neyin Konmayacağı
Hata gövdesinin tek yerden üretilmesi, ne konacağı kadar ne konmayacağını da denetlenebilir
kılar. Yığıt izleri, veritabanı sorgu metinleri, dosya yolları ve iç kimlikler istemciye
gitmez; bunlar instance alanıyla eşleştirilerek sunucu kütüğünde tutulur. İstemci bir
sorun bildirdiğinde olay kimliğini verir, tam bağlam kütükten bulunur.
Ölçüt, kimin ne yapabileceğine dayanır: istemcinin davranış değiştirebileceği bilgi gövdeye girer (kopya kalmadı, sınır iki, ISBN yok); istemcinin hiçbir şey yapamayacağı bilgi kütüğe girer. Bu ölçüt hem güvenlik hem kullanışlılık gerekçesiyle aynı sonucu verir.
Özet
- Hata gövdesi uç nokta başına yazıldığında sekiz hata durumu altı ayrı anahtar kümesi üretir ve hiçbirinde ortak çekirdek bulunmaz; istemci bu tabloyla tek bir hata sözleşmesi kuramaz.
- Problem ayrıntısı biçiminin beş çekirdek alanı ayrı işlere hizmet eder:
typesınıfı adlandırır,titlesınıfın sabit adıdır,statusdurum kodunu gövdede yineler,detailörneği anlatır,instancetek olayı kütükle eşler. - İstemci
typealanına göre dallanır;detailmetnine göre dallanmak, sunucudaki bir yazım düzeltmesini kırıcı değişiklik hâline getirir. - Gövdenin sorun türü kataloğuna dayanan tek katmandan üretilmesi ayrı çekirdek biçim sayısını bire indirir; ek üyeler uç noktaya değil sorun türüne bağlı olduğu için farklı kalabilir.
- Katalog, kendisinde olmayan bir tür istendiğinde hata vererek biçimin zamanla yeniden dağılmasını çalışma anında görünür kılar.
- İstemcinin davranışını değiştirebileceği bilgi gövdeye, değiştiremeyeceği bilgi
instanceile eşlenmiş kütük kaydına yazılır.
Sonraki Adım
Katalogdaki dogrulama türü bu derste hiç kullanılmadı. Nedeni, doğrulama hatasının tek
bir cümleye sığmamasıdır: gövdesinde dört alanı birden yanlış olan bir ödünç isteği “istek
gövdesi doğrulanamadı” başlığıyla yeterince anlatılamaz. İstemcinin her yanlış alanı kendi
giriş kutusunun yanında göstermesi gerekir, bunun için de sunucunun hangi alanın neden
reddedildiğini alan alan bildirmesi şarttır. Sonraki ders problem ayrıntısına alan düzeyinde
hata listesi ekler, alan adlarının istemcideki alanlarla hangi kurala göre eşleşeceğini
belirler ve eşleşmenin bozulduğu durumu ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.