İçeriğe geç
academia.sh

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ı type iç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: type sınıfı adlandırır, title sınıfın sabit adıdır, status durum kodunu gövdede yineler, detail örneği anlatır, instance tek olayı kütükle eşler.
  • İstemci type alanına göre dallanır; detail metnine 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 instance ile 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat