İçeriğe geç
academia.sh

Ders 11 / 16

Hata İşleme Katmanı

İstem hatası ile programcı hatası ayrılır ve tek bir katman her ikisini de aynı yanıt biçimine çevirir; istemciye giden metinler sabit bir tablodan gelir, iç ayrıntı yalnız kütükte kalır ve yanıt gövdelerinin sızıntı içermediği yedi senaryoda sınanır.

İçindekiler

Önceki dersin sunucusunda hata durumu tek bir dala sıkışmıştı: rafta bulunmayan kitap 409 üretiyordu. Gerçek bir uygulamada hatalar iki ayrı kaynaktan gelir. Üye eksik alanla istek gönderir; bu, uygulamanın öngördüğü bir durumdur ve yanıtı bilgilendiricidir. Uygulama kendi kodundaki bir kusurla çöker; bu, öngörülmemiş bir durumdur ve yanıtı bilgi vermemelidir.

Bu ders iki kaynağı ayırır ve ikisini de tek bir katmandan geçirir. Sonuçta her hata aynı biçimde yanıta dönüşür; aralarındaki fark, gövdenin ne kadarını taşıdığındadır.

İki Hata Sınıfı

İstem hatası (client error) uygulamanın beklediği bir durumdur: eksik alan, bozuk gövde, bulunmayan kaynak, çakışan durum. Bunlar kusur değildir; iş kuralının kendisidir. Yanıtları 4xx aralığındadır ve istemcinin ne yapması gerektiğini söylemelidir.

Programcı hatası (programmer error) uygulamanın kendi kusurudur: var olmayan bir fonksiyonun çağrılması, tanımsız bir değerin okunması, tutulmayan bir varsayım. Bunların yanıtı 5xx aralığındadır ve istemciye söyleyecek anlamlı hiçbir şeyleri yoktur — çünkü istemcinin yapabileceği bir şey yoktur.

Ayrımın Node.js Çalışma Zamanı kursunda kurulan işletimsel hata ve programcı hatası ayrımıyla ilişkisi doğrudandır; buradaki fark, ayrımın bir HTTP yanıtına çevrilmesidir.

Aşağıdaki modül ayrımı bir sınıfla ve bir çeviri fonksiyonuyla kurar.

// kaynak/kurulum/hatalar.mjs — istem hatasi ile programci hatasini ayirir ve yanita cevirir
export class IstemHatasi extends Error {
  constructor(kod, durum, ayrintilar = null) {
    super(`istem hatasi: ${kod}`);
    this.name = "IstemHatasi";
    this.kod = kod;                       // sabit tanimlayici; istemci buna gore dallanir
    this.durum = durum;
    this.ayrintilar = ayrintilar;         // istemciye gosterilmesi guvenli alanlar
  }
}

// Istemciye giden metinlerin tek kaynagi. Hata nesnesinin kendi iletisi yanita hic girmez.
const ILETILER = {
  govde_bicimsiz: "Istek govdesi JSON olarak cozulemedi.",
  alan_eksik: "Istek zorunlu alanlari tasimiyor.",
  kimlik_yok: "Gecerli bir uye kimligi gerekiyor.",
  kitap_yok: "Katalogda bu ISBN bulunmuyor.",
  raf_bos: "Kitabin rafta kopyasi kalmadi.",
  ic_hata: "Istek islenemedi.",
};

export const yanitaCevir = (hata, istekKimligi) => {
  const bilinen = hata instanceof IstemHatasi && hata.kod in ILETILER;
  const kod = bilinen ? hata.kod : "ic_hata";
  const govde = { hata: { kod, ileti: ILETILER[kod] }, istekKimligi };
  if (bilinen && hata.ayrintilar) govde.hata.ayrintilar = hata.ayrintilar;
  return { durum: bilinen ? hata.durum : 500, govde };
};

Üç karar bu otuz satırda gizlidir.

İstemciye giden metin hatadan gelmez. yanitaCevir fonksiyonu hata.message değerini hiç okumaz; iletiyi sabit bir tablodan alır. Hata iletileri geliştirici için yazılır ve dosya yolu, sorgu metni, iç değişken adı taşıyabilir. Tablodan gelen ileti bunu yapısal olarak imkânsız kılar.

Bilinmeyen her şey iç hatadır. Koşul yalnız IstemHatasi örneklerini değil, kodu tabloda bulunan örnekleri geçirir. Tabloya eklenmemiş bir kodla oluşturulmuş bir hata da ic_hata olur; varsayılan, sızdırmama yönündedir.

Ayrıntılar açıkça işaretlenir. ayrintilar alanı yalnız hatayı fırlatan kodun bilerek koyduğu değerleri taşır — eksik alan adları, sorgulanan ISBN. Bu, istemciye yardım eden bilgi ile iç ayrıntı arasındaki sınırı bir alan adına bağlar.

Tek Katmandan Geçirmek

Sunucunun bütün işleyicileri hata fırlatır; hiçbiri yanıt yazmaz. Yanıt yazma işi tek bir catch bloğundadır. Bu, önceki bölümde kurulan kuralın uygulanmasıdır: hata yakalayıcı, koruduğu her şeyin dışında durur.

// kaynak/http/sunucu.mjs — tek bir hata katmani: her hata ayni bicimde yanita cevrilir
import { createServer } from "node:http";
import { IstemHatasi, yanitaCevir } from "../kurulum/hatalar.mjs";

const KATALOG = new Map([["978-0262033848", { rafta: 0 }], ["978-0201835953", { rafta: 3 }]]);
let sayac = 0;

const govdeyiCoz = (metin) => {
  try {
    return metin ? JSON.parse(metin) : {};
  } catch {
    throw new IstemHatasi("govde_bicimsiz", 400);
  }
};

const oduncVer = (istem, uye) => {
  if (!uye) throw new IstemHatasi("kimlik_yok", 401);
  const eksik = ["isbn"].filter((alan) => !istem[alan]);
  if (eksik.length > 0) throw new IstemHatasi("alan_eksik", 400, { eksikAlanlar: eksik });
  const kitap = KATALOG.get(istem.isbn);
  if (!kitap) throw new IstemHatasi("kitap_yok", 404, { isbn: istem.isbn });
  if (kitap.rafta < 1) throw new IstemHatasi("raf_bos", 409, { isbn: istem.isbn });
  kitap.rafta -= 1;
  return { odunc: { isbn: istem.isbn, uye }, kalan: kitap.rafta };
};

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const kimlik = `i-${++sayac}`;
  const yaz = (durum, govde) => {
    const metin = JSON.stringify(govde);
    yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8",
      "content-length": Buffer.byteLength(metin), "x-istek-kimligi": kimlik });
    yanit.end(metin);
  };

  try {
    const parcalar = [];
    for await (const parca of istek) parcalar.push(parca);
    const yol = new URL(istek.url, "http://yerel").pathname;
    const uye = (istek.headers.authorization ?? "").replace(/^Uye /, "") || null;

    if (yol === "/odunc") return yaz(201, oduncVer(govdeyiCoz(Buffer.concat(parcalar).toString()), uye));
    if (yol === "/rapor") return yaz(200, { toplam: KATALOG.gec().length });   // programci hatasi
    throw new IstemHatasi("kitap_yok", 404);
  } catch (hata) {
    const beklenen = hata instanceof IstemHatasi;
    console.log(JSON.stringify({                                   // ic ayrinti yalniz kutukte
      duzey: beklenen ? "uyari" : "hata", istekKimligi: kimlik,
      tur: beklenen ? "istem_hatasi" : "programci_hatasi",
      ad: hata.name, ileti: hata.message,
      yigit: beklenen ? null : ((hata.stack ?? "").split("\n")[1] ?? "")
        .trim().replace(/\(file:.*\/(?=[^\/]+:\d+:\d+\))/, "("),
    }));
    const { durum, govde } = yanitaCevir(hata, kimlik);
    yaz(durum, govde);
  }
}).listen(8439, "127.0.0.1", () => console.log(JSON.stringify({ olay: "acildi", baglantiNoktasi: 8439 })));

/rapor yolundaki KATALOG.gec() çağrısı var olmayan bir yöntemi çağırır ve gerçek bir programcı hatası üretir; taklit edilmiş bir hata değil, yazım yanlışının doğal sonucudur.

Kütük kaydı iki hata sınıfını ayrı düzeylere koyar. İstem hataları uyari düzeyindedir ve yığıt izi taşımaz — çünkü kodun nerede olduğu bilinen bir şeydir. Programcı hataları hata düzeyindedir ve yığıt izinin ilk satırını taşır; kusuru bulmak için gereken tek bilgi budur.

Yedi Senaryonun Sınanması

Yanıt biçiminin doğruluğu okunarak değil, bütün dalları çalıştırılarak doğrulanır. Aşağıdaki sınama yedi senaryoyu sırayla dener, her yanıtın durum kodunu beklenenle karşılaştırır ve gövdede yasaklı kalıp arar.

Sınamanın ikinci yarısı önemlidir: yanıtların temiz olması, uygulamanın hataları sessizce yutmasıyla da sağlanabilirdi. Bunu dışlamak için sınama, iç ayrıntının kütükte gerçekten bulunduğunu da doğrular. İki yön birlikte sınandığında geçen bir sınama anlamlıdır.

// denetim/hata-yaniti.mjs — her hata yanitinin bicimini ve sizinti icermedigini sinar.
// Ayrica programci hatasinin ic ayrintisinin kutukte GERCEKTEN bulundugunu dogrular:
// sinamanin sessizce her seyi yutan bir uygulamayi onaylamamasi icin iki yon de gereklidir.
import { spawn } from "node:child_process";
import { setTimeout as bekle } from "node:timers/promises";

const YASAK = [/at\s+\S+\s+\(/, /\.mjs/, /node:/, /TypeError/, /\/Users\//, /istem hatasi:/];

const DURUMLAR = [
  ["gecerli istek", "/odunc", "Uye U-4711", '{"isbn":"978-0201835953"}', 201],
  ["kimliksiz istek", "/odunc", null, '{"isbn":"978-0201835953"}', 401],
  ["bozuk govde", "/odunc", "Uye U-4711", "{isbn", 400],
  ["eksik alan", "/odunc", "Uye U-4711", "{}", 400],
  ["katalogda yok", "/odunc", "Uye U-4711", '{"isbn":"978-0000000000"}', 404],
  ["raf bos", "/odunc", "Uye U-4711", '{"isbn":"978-0262033848"}', 409],
  ["programci hatasi", "/rapor", "Uye U-4711", null, 500],
];

const cocuk = spawn("node", ["kaynak/http/sunucu.mjs"]);
let kutuk = "";
cocuk.stdout.on("data", (p) => (kutuk += p));
cocuk.stderr.on("data", (p) => (kutuk += p));
await bekle(700);

const kusurlar = [];
console.log(`${"durum".padEnd(6)} | ${"senaryo".padEnd(18)} | yanit govdesi`);
for (const [ad, yol, kimlik, govde, beklenen] of DURUMLAR) {
  const yanit = await fetch(`http://127.0.0.1:8439${yol}`, {
    method: govde === null ? "GET" : "POST",
    headers: { ...(kimlik ? { authorization: kimlik } : {}),
      ...(govde === null ? {} : { "content-type": "application/json" }) },
    body: govde,
  });
  const metin = await yanit.text();
  console.log(`${String(yanit.status).padEnd(6)} | ${ad.padEnd(18)} | ${metin}`);
  if (yanit.status !== beklenen) kusurlar.push(`${ad}: ${beklenen} bekleniyordu, ${yanit.status} geldi`);
  for (const kalip of YASAK) {
    if (kalip.test(metin)) kusurlar.push(`${ad}: yanit govdesinde yasak kalip ${kalip}`);
  }
}

await bekle(200);
cocuk.kill();
await bekle(300);

console.log("--- kutuk ---");
console.log(kutuk.trimEnd().split("\n").map((s) => "  " + s).join("\n"));

if (!/programci_hatasi/.test(kutuk)) kusurlar.push("kutukte programci hatasi kaydi yok");
if (!/TypeError/.test(kutuk)) kusurlar.push("kutukte ic hata turu yok");

console.log("--- denetim ---");
for (const kusur of kusurlar) console.log("  KUSUR  " + kusur);
console.log(kusurlar.length === 0
  ? "  yedi yanit da bicime uygun; ic ayrinti yalniz kutukte"
  : `  ${kusurlar.length} kusur`);
process.exit(kusurlar.length === 0 ? 0 : 1);
durum  | senaryo            | yanit govdesi
201    | gecerli istek      | {"odunc":{"isbn":"978-0201835953","uye":"U-4711"},"kalan":2}
401    | kimliksiz istek    | {"hata":{"kod":"kimlik_yok","ileti":"Gecerli bir uye kimligi gerekiyor."},"istekKimligi":"i-2"}
400    | bozuk govde        | {"hata":{"kod":"govde_bicimsiz","ileti":"Istek govdesi JSON olarak cozulemedi."},"istekKimligi":"i-3"}
400    | eksik alan         | {"hata":{"kod":"alan_eksik","ileti":"Istek zorunlu alanlari tasimiyor.","ayrintilar":{"eksikAlanlar":["isbn"]}},"istekKimligi":"i-4"}
404    | katalogda yok      | {"hata":{"kod":"kitap_yok","ileti":"Katalogda bu ISBN bulunmuyor.","ayrintilar":{"isbn":"978-0000000000"}},"istekKimligi":"i-5"}
409    | raf bos            | {"hata":{"kod":"raf_bos","ileti":"Kitabin rafta kopyasi kalmadi.","ayrintilar":{"isbn":"978-0262033848"}},"istekKimligi":"i-6"}
500    | programci hatasi   | {"hata":{"kod":"ic_hata","ileti":"Istek islenemedi."},"istekKimligi":"i-7"}
--- kutuk ---
  {"olay":"acildi","baglantiNoktasi":8439}
  {"duzey":"uyari","istekKimligi":"i-2","tur":"istem_hatasi","ad":"IstemHatasi","ileti":"istem hatasi: kimlik_yok","yigit":null}
  {"duzey":"uyari","istekKimligi":"i-3","tur":"istem_hatasi","ad":"IstemHatasi","ileti":"istem hatasi: govde_bicimsiz","yigit":null}
  {"duzey":"uyari","istekKimligi":"i-4","tur":"istem_hatasi","ad":"IstemHatasi","ileti":"istem hatasi: alan_eksik","yigit":null}
  {"duzey":"uyari","istekKimligi":"i-5","tur":"istem_hatasi","ad":"IstemHatasi","ileti":"istem hatasi: kitap_yok","yigit":null}
  {"duzey":"uyari","istekKimligi":"i-6","tur":"istem_hatasi","ad":"IstemHatasi","ileti":"istem hatasi: raf_bos","yigit":null}
  {"duzey":"hata","istekKimligi":"i-7","tur":"programci_hatasi","ad":"TypeError","ileti":"KATALOG.gec is not a function","yigit":"at Server.<anonymous> (sunucu.mjs:44:61)"}
--- denetim ---
  yedi yanit da bicime uygun; ic ayrinti yalniz kutukte

Sınama sıfır çıkış kodu üretir. Yığıt izindeki satır ve sütun numarası kodun her değişikliğinde kayar.

Ölçümün Söyledikleri

Altı hata yanıtı aynı biçimdedir. Hepsinde hata.kod, hata.ileti ve istekKimligi vardır; ikisinde ek olarak ayrintilar bulunur. İstemci tarafında tek bir çözümleyici bütün hataları işleyebilir ve dallanmayı kod alanına dayandırır. İletinin kendisi görüntülemek içindir; karar kod ile verilir, çünkü ileti değişebilir, kod değişmez.

Ayrıntılar yalnız yararlı olduğunda vardır. alan_eksik yanıtı eksik alanların adını verir, kitap_yok yanıtı sorgulanan ISBN’i döndürür. Bunlar istemcinin zaten bildiği ya da düzeltmesi gereken değerlerdir. Kimlik hatası yanıtında hiçbir ayrıntı yoktur; hangi kimliğin neden geçersiz olduğunu söylemek, kimlik denemesi yapan bir tarafa geri bildirim vermek demektir.

Beşinci senaryo bir ayrımı görünür kılar. Katalogda bulunmayan ISBN 404, rafta kopyası kalmayan kitap 409 alır. İkisi de “veremiyorum” der, ama biri kaynağın hiç olmadığını, diğeri kaynağın var olup şu an uygun olmadığını bildirir. İstemci ilkinde girdiyi düzeltir, ikincisinde bekler.

Yedinci senaryo ayrımın asıl noktasıdır. İstemciye giden gövde yalnız ic_hata kodunu, sabit tablodan gelen iletiyi ve i-7 kimliğini taşır. Kütükte ise aynı isteğin TypeError olduğu, iletisinin KATALOG.gec is not a function olduğu ve yığıt izinin ilk satırı bulunur. Aynı olay iki yerde iki farklı ayrıntı düzeyiyle kayıtlıdır ve i-7 kimliğiyle eşleşir.

Sınama iki yönlüdür. Yanıtlarda yasaklı kalıp bulunmaması tek başına yetmez; kütükte programci_hatasi ve TypeError kayıtlarının bulunması da sınanır. Hataları sessizce yutan bir uygulama ilk koşulu geçer, ikincisinde kalır.

Özet

  • Hatalar iki sınıfa ayrılır: istem hatası uygulamanın öngördüğü bir durumdur ve 4xx üretir, programcı hatası uygulamanın kusurudur ve 5xx üretir.
  • İstemciye giden ileti hata nesnesinden değil, sabit bir tablodan gelir; tabloda karşılığı olmayan her hata ic_hata sayılır, yani varsayılan sızdırmama yönündedir.
  • İstemciye gösterilebilir alanlar ayrintilar başlığı altında açıkça işaretlenir; kimlik hatasında ayrıntı verilmez.
  • Bütün işleyiciler hata fırlatır, yanıtı tek bir katman yazar; kütük kaydı istem hatalarını uyari, programcı hatalarını hata düzeyinde tutar ve yığıt izini yalnız ikincisine ekler.
  • Sınama yedi senaryonun yanıt gövdesinde yasaklı kalıp aramakla kalmaz, iç ayrıntının kütükte bulunduğunu da doğrular; iki yön birlikte sınandığında hataları yutan bir uygulama sınamayı geçemez.

Sonraki Adım

Bu bölümde kurulan parçalar — katmanlı dizin düzeni, ortamdan okunan ve açılışta doğrulanan yapılandırma, gizli değerlerin ayrılması, standart çıktıya yazılan yapılandırılmış kütük ve ortak hata yanıtı — birbirinden bağımsız kararlar gibi görünür. Aralarında bir bağ vardır ve bu bağ tek bir hedefte toplanır: uygulamanın çalıştığı makineden bağımsız olması. Sonraki ders bu kararları adlandırılmış bir ilkeler kümesi olarak toplar, her ilkenin bu bölümde hangi ölçümle karşılandığını gösterir ve karşılanmayanları işaret eder.

İ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