---
title: 'Hata Yanıt Biçimi'
source: 'https://academia.sh/tr/kurslar/api-tasarimi/hata-yanit-bicimi'
course: 'Web API Tasarımı'
language: tr
updated: '2026-08-17T18:06:44+00:00'
license: 'CC BY-SA 4.0'
---

# 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ı.

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.

```js
// 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,
    },
  };
}
```

```js
// 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ü.

```js
// 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}`);
```

```bash
#!/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.

```bash
#!/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.
