Ders 19 / 34
Doğrulama Hataları
Alan düzeyinde hata listesinin problem ayrıntısına eklenmesi, ilk hatada durmakla hepsini toplamanın istek turu farkı ve alan adlarının istemcideki giriş kutularıyla eşleşmesini sağlayan gövde yolu kuralı.
İçindekiler
Bir önceki ders hata gövdesini tek biçime bağladı ama sorun kataloğundaki dogrulama
türünü kullanmadan bıraktı. Nedeni, bu türün diğerlerinden yapısal olarak farklı olmasıdır.
“Kopya yok” tek bir olgudur; detail alanındaki bir cümle onu tümüyle anlatır. “İstek
gövdesi doğrulanamadı” ise bir olgu değil, bir olgular kümesidir: gövdenin hangi
alanları, hangi kurallara göre reddedildi?
Ödünç servisinin ödünç verme isteği artık tek kitaplık değildir; bir üye, birden çok kalem ve bir iade tarihi taşır. Bu ders alan düzeyinde hata bildirimini kurar ve iki kararı ölçerek verir: hataların kaçının bir seferde bildirileceği ve alanların hangi adla anılacağı.
Bir Seferde Kaç Hata
Doğrulama, ilk kuralı çiğneyen alanda durabilir ya da bütün gövdeyi tarayıp hataları toplayabilir. Aradaki fark, kullanıcının formu düzeltmek için kaç kez istek yollayacağıdır.
Aşağıdaki doğrulayıcı şemayı veri olarak alır. Şema anahtarları gövde yoludur ve JSON
Pointer biçiminde yazılır: /uye, /kalemler/1/isbn. * işareti dizi öğeleri üzerinde
açılır, yani /kalemler/*/isbn kalıbı gövdedeki her kaleme uygulanır.
// dogrula.mjs — govdeyi semaya gore dogrular, alan duzeyinde hata listesi uretir // Alan adi JSON Pointer (RFC 6901): "/uye", "/kalemler/1/isbn" export const KURALLAR = { zorunlu: (d) => (d === undefined || d === null || d === "" ? { kod: "zorunlu" } : null), desen: (d, p) => (new RegExp(p).test(String(d)) ? null : { kod: "bicim", param: p }), secenek: (d, p) => (p.includes(d) ? null : { kod: "secenek_disi", param: p }), dizi: (d, p) => !Array.isArray(d) ? { kod: "dizi_degil" } : d.length < p.enAz ? { kod: "cok_az", param: p } : d.length > p.enCok ? { kod: "cok_cok", param: p } : null, tarih: (d) => (/^\d{4}-\d{2}-\d{2}$/.test(String(d)) ? null : { kod: "tarih_degil" }), enCokGun: (d, p, bugun) => { const fark = Math.round((Date.parse(d) - Date.parse(bugun)) / 86400000); if (Number.isNaN(fark)) return null; // tarih kurali zaten bildirdi if (fark < 0) return { kod: "gecmiste" }; return fark > p ? { kod: "cok_ileri", param: p } : null; }, }; // Yol icindeki * gercek dizi dizinleriyle acilir: /kalemler/*/isbn -> /kalemler/0/isbn const oku = (govde, yol) => yol.split("/").slice(1).reduce((d, p) => (d == null ? undefined : d[p]), govde); function yollariAc(govde, kalip) { const yildiz = kalip.indexOf("/*"); if (yildiz < 0) return [kalip]; const dizi = oku(govde, kalip.slice(0, yildiz)); if (!Array.isArray(dizi)) return []; return dizi.flatMap((_, i) => yollariAc(govde, kalip.slice(0, yildiz) + `/${i}` + kalip.slice(yildiz + 2))); } // dogrula(sema, govde, bugun) -> [{ yol, kod, param }] export function dogrula(sema, govde, bugun) { const hatalar = []; for (const [kalip, kurallar] of Object.entries(sema)) { for (const yol of yollariAc(govde, kalip)) { const deger = oku(govde, yol); for (const [ad, param] of kurallar) { if (ad !== "zorunlu" && (deger === undefined || deger === null || deger === "")) break; const sonuc = KURALLAR[ad](deger, param, bugun); if (sonuc) { hatalar.push({ yol, ...sonuc }); break; } // alan basina tek hata } } } return hatalar; }
İki toplama biçimini karşılaştırmak için, dört alanı birden yanlış olan bir gövde alınır ve kullanıcının yalnızca kendisine bildirilen hataları düzelttiği varsayılır.
// tur-tur.mjs — ilk hatada duran dogrulama ile hepsini toplayan dogrulamayi karsilastirir import { dogrula } from "./dogrula.mjs"; const BUGUN = "2026-03-01"; const SEMA = { "/uye": [["zorunlu"], ["desen", "^U-\\d{4}$"]], "/kalemler": [["zorunlu"], ["dizi", { enAz: 1, enCok: 3 }]], "/kalemler/*/isbn": [["zorunlu"], ["desen", "^97[89]-\\d{10}$"]], "/kalemler/*/sube": [["zorunlu"], ["secenek", ["merkez", "sahil", "tepe"]]], "/iadeTarihi": [["zorunlu"], ["tarih"], ["enCokGun", 30]], }; // Kullanicinin ilk gonderdigi govde: dort alan birden yanlis. const ILK = { uye: "1001", kalemler: [ { isbn: "978-0262033848", sube: "merkez" }, { isbn: "0262033848", sube: "deniz" }, ], iadeTarihi: "2026-06-01", }; // Kullanicinin duzeltme sirasi: hangi hata bildirilirse o duzeltilir. const DUZELTME = { "/uye": (g) => (g.uye = "U-1001"), "/kalemler/1/isbn": (g) => (g.kalemler[1].isbn = "978-0201896831"), "/kalemler/1/sube": (g) => (g.kalemler[1].sube = "sahil"), "/iadeTarihi": (g) => (g.iadeTarihi = "2026-03-20"), }; function turSayisi(hepsiniTopla) { const govde = structuredClone(ILK); let tur = 0; while (tur < 10) { tur++; const hatalar = dogrula(SEMA, govde, BUGUN); if (hatalar.length === 0) return tur; const bildirilen = hepsiniTopla ? hatalar : [hatalar[0]]; console.log(` tur ${tur}: ${bildirilen.map((h) => `${h.yol}=${h.kod}`).join(" ")}`); for (const h of bildirilen) DUZELTME[h.yol](govde); } return tur; } console.log("ilk hatada duran doğrulama:"); const a = turSayisi(false); console.log(` başarılı olana kadar istek turu: ${a}\n`); console.log("hepsini toplayan doğrulama:"); const b = turSayisi(true); console.log(` başarılı olana kadar istek turu: ${b}`);
ilk hatada duran doğrulama: tur 1: /uye=bicim tur 2: /kalemler/1/isbn=bicim tur 3: /kalemler/1/sube=secenek_disi tur 4: /iadeTarihi=cok_ileri başarılı olana kadar istek turu: 5 hepsini toplayan doğrulama: tur 1: /uye=bicim /kalemler/1/isbn=bicim /kalemler/1/sube=secenek_disi /iadeTarihi=cok_ileri başarılı olana kadar istek turu: 2
Beş tura karşı iki tur. Sayının kendisi gövdedeki yanlış alan sayısına bağlıdır — yanlış alan, ilk hatada duran doğrulamada tur demektir — ama davranış farkı sayıdan bağımsızdır: ilk hatada durmak, kullanıcıya bir sonraki hatanın varlığını saklar. Kullanıcı her düzeltmeden sonra işin bittiğini sanır ve yeni bir ret alır.
Bir ayrıntı korunmuştur: doğrulayıcı alan başına ilk hatada durur. /uye alanı hem
zorunlu hem desene uygun olmalıdır; boş bırakıldığında “zorunlu” ve “biçim” hatalarının
ikisini birden bildirmenin bir yararı yoktur, ikincisi birincinin sonucudur. Kural şudur:
alanlar arasında tarama sürer, alan içinde ilk hata yeter.
Hata Kaydının Üç Alanı
Her hata kaydı üç şey taşır. yol, gövdedeki alanın adresidir. kod, hatanın
kararlı kimliğidir — Frontend müfredatındaki Uygulama Mimarisi kursunda kurulan doğrulama
şemalarıyla aynı ayrım: kod dallanmak içindir, ileti göstermek için. param, kodun
gerektirdiği sayıdır: hangi desen, hangi seçenekler, kaç gün.
Kodun ileti yerine geçmesi, kullanıcıya gösterilecek metni istemci tarafında üretmeyi
mümkün kılar. Sunucu cok_ileri ve 30 gönderir; istemci bunu kendi dilinde ve kendi
tonunda cümleye çevirir. Sunucu metin gönderseydi çeviri, çoğul kuralı ve ton kararı
sunucuya taşınırdı; hiçbiri sunucunun işi değildir.
Bu üçlü, problem ayrıntısına bir ek üye olarak eklenir. Çekirdek beş alan yerinde
kalır; errors dizisi onların yanına gelir.
// sunucu.mjs — odunc istegini dogrulayip 422 problem ayrintisi donduren servis import { createServer } from "node:http"; import { dogrula } from "./dogrula.mjs"; const BUGUN = "2026-03-01"; // ornegin yinelenebilir olmasi icin sabit const SEMA = { "/uye": [["zorunlu"], ["desen", "^U-\\d{4}$"]], "/kalemler": [["zorunlu"], ["dizi", { enAz: 1, enCok: 3 }]], "/kalemler/*/isbn": [["zorunlu"], ["desen", "^97[89]-\\d{10}$"]], "/kalemler/*/sube": [["zorunlu"], ["secenek", ["merkez", "sahil", "tepe"]]], "/iadeTarihi": [["zorunlu"], ["tarih"], ["enCokGun", 30]], }; // Bir onceki derste yazilan sorun katalogunun bu derse dusen tek girdisi. let sayac = 0; const dogrulamaSorunu = (hatalar) => ({ type: "https://ornek.kutuphane/sorunlar/dogrulama", title: "İstek gövdesi doğrulanamadı", status: 422, detail: `${hatalar.length} alan kabul edilmedi.`, instance: `ol-${String(++sayac).padStart(4, "0")}`, errors: hatalar, }); const govdeOku = (istek) => new Promise((coz) => { let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(v)); }); // Cekirdek alanlar alt alta, her hata tek satir: govde okunabilir kalsin. const bicimle = (s) => [ "{", ...["type", "title", "status", "detail", "instance"].map((a) => ` "${a}": ${JSON.stringify(s[a])},`), ' "errors": [', ...s.errors.map((h, i) => ` ${JSON.stringify(h)}${i < s.errors.length - 1 ? "," : ""}`), " ]", "}", ].join("\n"); createServer(async (istek, yanit) => { yanit.sendDate = false; const govde = JSON.parse((await govdeOku(istek)) || "{}"); const hatalar = dogrula(SEMA, govde, BUGUN); if (hatalar.length) { yanit.writeHead(422, { "content-type": "application/problem+json; charset=utf-8" }); return yanit.end(bicimle(dogrulamaSorunu(hatalar))); } yanit.writeHead(201, { "content-type": "application/json; charset=utf-8" }); yanit.end(JSON.stringify({ id: "O-2", uye: govde.uye, kalem: govde.kalemler.length })); }).listen(8433, "127.0.0.1", () => console.log("dogrulama sunucusu 127.0.0.1:8433"));
#!/usr/bin/env bash # Dort alani yanlis bir odunc istegi yollar, 422 govdesini gosterir. node sunucu.mjs & sunucu=$! sleep 0.5 curl -sS -w '\n[%{http_code}] %{content_type}\n' -X POST -H 'content-type: application/json' \ -d '{"uye":"1001","kalemler":[{"isbn":"978-0262033848","sube":"merkez"},{"isbn":"0262033848","sube":"deniz"}],"iadeTarihi":"2026-06-01"}' \ http://127.0.0.1:8433/odunc echo "--- duzeltilmis govde ---" curl -sS -w '\n[%{http_code}] %{content_type}\n' -X POST -H 'content-type: application/json' \ -d '{"uye":"U-1001","kalemler":[{"isbn":"978-0262033848","sube":"merkez"},{"isbn":"978-0201896831","sube":"sahil"}],"iadeTarihi":"2026-03-20"}' \ http://127.0.0.1:8433/odunc kill "$sunucu"; wait "$sunucu" 2>/dev/null
dogrulama sunucusu 127.0.0.1:8433
{
"type": "https://ornek.kutuphane/sorunlar/dogrulama",
"title": "İstek gövdesi doğrulanamadı",
"status": 422,
"detail": "4 alan kabul edilmedi.",
"instance": "ol-0001",
"errors": [
{"yol":"/uye","kod":"bicim","param":"^U-\\d{4}$"},
{"yol":"/kalemler/1/isbn","kod":"bicim","param":"^97[89]-\\d{10}$"},
{"yol":"/kalemler/1/sube","kod":"secenek_disi","param":["merkez","sahil","tepe"]},
{"yol":"/iadeTarihi","kod":"cok_ileri","param":30}
]
}
[422] application/problem+json; charset=utf-8
--- duzeltilmis govde ---
{"id":"O-2","uye":"U-1001","kalem":2}
[201] application/json; charset=utf-8
Durum kodunun 400 değil 422 olması bilinçli bir seçimdir. 400, isteğin sunucu tarafından anlaşılamadığını bildirir; bozuk JSON, eksik başlık, tanınmayan içerik tipi. Buradaki gövde eksiksiz anlaşılmıştır — ayrıştırılmış, alanları okunmuş, kurallara sokulmuştur — ve anlamı reddedilmiştir. Ayrım kütüğe yansıdığında da işe yarar: 400 oranındaki artış istemcinin isteği yanlış kurduğunu, 422 oranındaki artış kullanıcının yanlış veri girdiğini gösterir; ikisinin nedeni ve çözümü farklıdır.
Alan Adının İstemcideki Karşılığı
Hata listesinin işe yaraması, istemcinin her hatayı hangi giriş kutusunun yanında göstereceğini bilmesine bağlıdır. Erişilebilir Bileşen Kalıpları kursundaki hata sunumu bu eşleşmeyi varsayar: her hata kendi alanının ek açıklamasına bağlanır, hata özeti odağı o alana taşır. Eşleşme kurulamazsa geriye tek bir genel ileti kalır.
Aşağıdaki ölçüm aynı dört hatayı üç ayrı sunucu sözleşmesiyle bildirir ve istemcinin kaçını kutusuyla eşleştirebildiğini sayar.
// eslesme.mjs — uc ayri alan adlandirma sozlesmesinin istemcide eslesme oranini olcer // Istemcideki giris kutulari, sozlesmedeki kurala gore adlandirilmistir: // ad = JSON Pointer'in bas egik cizgisi atilmis, "/" yerine "." konmus hali. const KUTULAR = ["uye", "kalemler.0.isbn", "kalemler.0.sube", "kalemler.1.isbn", "kalemler.1.sube", "iadeTarihi"]; const adaCevir = (yol) => yol.replace(/^\//, "").replaceAll("/", "."); // Ayni dort hata, uc ayri sunucu sozlesmesiyle bildirilmis hali. const SOZLESMELER = { "alan adı yok": { hatalar: [{ ileti: "Gönderdiğiniz bilgilerde dört hata var." }], cevir: () => null, }, "sunucunun iç adları": { hatalar: [ { alan: "member_id", kod: "bicim" }, { alan: "items[1].isbn", kod: "bicim" }, { alan: "items[1].branch", kod: "secenek_disi" }, { alan: "due_date", kod: "cok_ileri" }, ], cevir: (h) => h.alan, }, "gövde yolu (JSON Pointer)": { hatalar: [ { yol: "/uye", kod: "bicim" }, { yol: "/kalemler/1/isbn", kod: "bicim" }, { yol: "/kalemler/1/sube", kod: "secenek_disi" }, { yol: "/iadeTarihi", kod: "cok_ileri" }, ], cevir: (h) => adaCevir(h.yol), }, }; console.log("sözleşme bildirilen eşleşen kutusunun yanında gösterilebilen"); for (const [ad, { hatalar, cevir }] of Object.entries(SOZLESMELER)) { const eslesen = hatalar.filter((h) => KUTULAR.includes(cevir(h))); console.log( `${ad.padEnd(27)} ${String(hatalar.length).padStart(9)} ${String(eslesen.length).padStart(7)} ` + `${eslesen.length ? eslesen.map((h) => cevir(h)).join(", ") : "hiçbiri"}` ); }
sözleşme bildirilen eşleşen kutusunun yanında gösterilebilen alan adı yok 1 0 hiçbiri sunucunun iç adları 4 0 hiçbiri gövde yolu (JSON Pointer) 4 4 uye, kalemler.1.isbn, kalemler.1.sube, iadeTarihi
İkinci satır ilkinden daha kötüdür, çünkü aldatıcıdır: sunucu dört ayrı hata bildirmiş
görünür, alan adları da vardır, ama o adlar sunucunun kendi iç veri modelinden gelir.
member_id, items[1].branch — istemcinin gövdesinde böyle bir alan yoktur. İstemci ya
sunucunun iç adlarıyla kendi alan adları arasında elle bir çeviri tablosu tutar ya da
hataları eşleştirmeden gösterir. Çeviri tablosu tutulursa sunucunun her iç yeniden
adlandırması istemciyi bozar; yani sözleşmede olmayan bir şey sözleşme gibi davranmaya
başlar.
Kural şudur: hata yolu, istemcinin gönderdiği gövdedeki alanın adresidir. Sunucunun iç model adları değil, istek gövdesinin yapısı. Bu kural iki özelliği garanti eder. Toplamdır — gövdedeki her alanın bir yolu vardır, dizi öğeleri dâhil. Ve tek yönlü türetilebilir: istemci gövdeyi kendisi kurduğu için yolu kutu adına çevirecek kuralı yazabilir, ters yönde bir tablo tutmasına gerek kalmaz.
Kalan tek karar, gövde yolunun hangi biçimde yazılacağıdır. JSON Pointer tanımlı bir standarttır, dizi dizinlerini doğal biçimde ifade eder ve kaçırma kuralları belirlidir. Yerine nokta ayırıcı da seçilebilir; belirleyici olan biçimin kendisi değil, belgelenmiş ve her alan için tanımlı olmasıdır.
Özet
- Doğrulama ilk hatada durduğunda yanlış alan istek turu gerektirir; hatalar toplu bildirildiğinde iki tur yeter ve kullanıcıdan bir sonraki hatanın varlığı saklanmaz.
- Alanlar arasında tarama sürer, alan içinde ilk hata yeter: aynı alanın ardışık kural ihlalleri birbirinin sonucudur.
- Hata kaydı üç alan taşır — yol, kod, parametre; kullanıcıya gösterilecek metin sunucudan değil koddan ve parametreden istemcide üretilir.
- Anlaşılamayan istek 400, anlaşılıp anlamı reddedilen istek 422 ile bildirilir; ayrım kütükteki artışın nedenini de ayırır.
- Hata yolu istemcinin gönderdiği gövdedeki adrestir, sunucunun iç model adı değil; iç adlar bildirildiğinde eşleşme oranı dörtte sıfıra düşer.
- Alan adlandırma sözleşmesi toplam ve tek yönlü türetilebilir olmalıdır; hangi biçimin seçildiği değil, belgelenmiş ve her alan için tanımlı olması belirleyicidir.
Sonraki Adım
Doğrulama şeması bu derste sabit kabul edildi: /kalemler en çok üç öğe alır, iade tarihi
en çok otuz gün ileri olabilir. Kütüphane bu sınırları değiştirmeye karar verdiğinde ne
olacak? Kalem sınırının beşe çıkması hiçbir istemciyi bozmaz; üçe düşmesi, dört kalemlik
istek yollayan her istemciyi bozar. Aynı gövde alanının adının değişmesi ise bütün
istemcileri aynı anda bozar. Sözleşmenin değişmesi kaçınılmazdır; sonraki ders bu
değişikliği yönetilebilir kılan aracı ele alır ve aynı kaynağın iki sürümünü tek sunucuda
yayımlayarak yol, başlık ve içerik tabanlı sürümlemenin istemci koduna maliyetini
karşılaştırır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.