Problem Details, HTTP API'lerinin hata ayrıntılarını makine tarafından okunabilir biçimde iletmesi için tanımlanmış standart bir JSON nesnesidir. Güncel tanımı RFC 9457'dir ve bu belge eski RFC 7807'nin yerini almıştır. Nesnenin beş tanımlı üyesi vardır: type, title, status, detail ve instance; bunlara probleme özel uzantı alanları eklenebilir. Yanıt Content-Type: application/problem+json ile gönderilir.
Birçok kaynak, incelediğimiz tasarım rehberi de dahil, hâlâ RFC 7807 adını kullanır. İki belge arasındaki fark küçüktür ve RFC 9457'nin ekinde listelenir: ortak problem türleri için bir kayıt (registry) oluşturulmuş, birden çok problemin nasıl ele alınacağı netleştirilmiş ve çözümlenemeyen (non-resolvable) tür URI'leri için yönlendirme eklenmiştir. Bu yazıda güncel metni, yani RFC 9457'yi esas alıyoruz. Kendi özel hata biçiminizi tasarlamak isterseniz hata mesajı formatı ve hata kodları yazısına bakın.
Neden standart bir hata biçimi?

HTTP durum kodları bir hatanın genel sınıfını söyler ama çoğu zaman istemcinin ne yapması gerektiğini anlatmaya yetmez. RFC 9457 bunu kendi örneğiyle açıklar: hesabında yeterli bakiye olmayan bir kullanıcıya 403 Forbidden dönmek; ara katmanlara, önbelleklere ve istemci kütüphanelerine yanıtın genel anlamını bildirir. Ama reddin nedeni ve mevcut bakiye gibi API'ye özel ayrıntılar gövdede taşınmalıdır ki istemci uygun eylemi (örneğin bakiye yükleme ekranını açmayı) seçebilsin. İnsanlar tarayıcıda bir HTML hata sayfasını okuyabilir; bir program ise okuyamaz.
RFC'nin belirttiği amaç, hata biçimine ihtiyaç duyan uygulamaların kendi biçimlerini icat etmek zorunda kalmaması ya da daha kötüsü mevcut durum kodlarının anlamını yeniden tanımlamaya yönelmemesidir. Standart bir biçimin pratik faydası, aynı istemci kodunun farklı API'lerden gelen hataları aynı şekilde ayrıştırabilmesidir.
Örnek: yetersiz bakiye
Bir mağaza API'sine sipariş veren istemci, hata yanıtında Problem Details kabul ettiğini Accept başlığında bildirebilir:
POST /siparisler HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json, application/problem+json
{
"urun_id": 123456,
"adet": 2
}Bakiye yetmediğinde sunucu şu yanıtı döner. Örnek, RFC 9457'deki "out of credit" örneğinin Türkçeleştirilmiş bir uyarlamasıdır:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: tr
{
"type": "https://api.example.com/problemler/yetersiz-bakiye",
"title": "Bakiyeniz yetersiz.",
"status": 403,
"detail": "Mevcut bakiyeniz 30 TL, bu sipariş 50 TL tutuyor.",
"instance": "/hesaplar/12345/islemler/abc",
"bakiye": 30,
"hesaplar": ["/hesaplar/12345", "/hesaplar/67890"]
}Bu yanıtta type problemin türünü, title o türün kısa adını, detail bu olaya özgü açıklamayı, instance bu olayın kendisini tanımlıyor. bakiye ve hesaplar ise bu problem türünün tanımladığı iki uzantı alanı: biri mevcut bakiyeyi, öteki bakiye yüklenebilecek hesapların adreslerini veriyor. Content-Language başlığı insan için yazılmış metinlerin dilini bildiriyor.
RFC, sunucunun istemci Accept başlığında listelemese bile application/problem+json ile yanıt verebileceğini de not eder; HTTP buna izin verir. İnsan için yazılmış metinlerin dili Accept-Language ile anlaşılabilir, ama bu anlaşma yine de varsayılan dilde bir yanıtla sonuçlanabilir.
Beş üye tek tek

RFC 9457 genel bir kural koyar: bir üyenin değer tipi tanımdakiyle uyuşmuyorsa o üye yok sayılmalıdır (MUST); işlem, üye hiç gönderilmemiş gibi sürer. Örneğin status sayı yerine metin olarak gelirse istemci onu görmezden gelir. Üyelerin ayrıntıları şöyle:
type
Problem türünü tanımlayan bir URI başvurusu (URI reference) içeren metindir ve tüketici bu URI'yi problem türünün birincil tanımlayıcısı olarak kullanmalıdır (MUST). Üye yoksa değeri about:blank kabul edilir. URI https gibi erişilebilir bir adresse, adrese gidildiğinde insan için okunabilir bir belge dönmesi önerilir (SHOULD); ancak istemciler bu adresi kendiliğinden çağırmamalıdır (SHOULD NOT), yalnızca geliştiriciye bilgi gösterilirken örneğin bir hata ayıklama aracında açılabilir. Göreli URI kullanılabilir ama farklı kaynaklardan dönen aynı göreli değer farklı adreslere çözümleneceği için karışıklık yaratır; RFC mutlak URI önerir, göreli kullanılacaksa tam yol (/types/123 gibi) içermesini ister.
Tür URI'sinin çözümlenebilir olması zorunlu değildir; örneğin tag: şeması da kullanılabilir. Ama RFC çözümlenebilir URI'leri teşvik eder: ileride türleri çözen bir araç benimsediğinizde çözümlenemeyen bir URI'den vazgeçmek, problem türü için yeni bir kimlik üretmek, yani kırıcı (breaking) bir değişiklik anlamına gelir.
status
Kaynak sunucunun bu olay için ürettiği HTTP durum kodunu taşıyan bir sayıdır. Yalnızca bilgi amaçlıdır; üreten taraf gerçek HTTP yanıtında aynı kodu kullanmak zorundadır (MUST). Böylece bu biçimi tanımayan genel HTTP yazılımları yine doğru davranır. status özellikle gövde HTTP bilgisi olmadan saklandığında ya da bir ara katman durum kodunu değiştirdiğinde asıl kodu görmek için işe yarar.
title
Problem türünün kısa, insan için okunabilir özetidir. Olaydan olaya değişmemelidir (SHOULD NOT); tek istisnası yerelleştirmedir. Bilgi amaçlıdır ve tür URI'sinin anlamını keşfedemeyen kullanıcılar için, örneğin çevrim dışı günlük incelemesinde, yer alır.
detail
Bu olaya özgü, insan için yazılmış açıklamadır. Hata ayıklama bilgisi vermek yerine istemcinin sorunu düzeltmesine yardım etmeye odaklanmalıdır. Tüketiciler detail metnini ayrıştırarak bilgi çıkarmaya çalışmamalıdır (SHOULD NOT); makinenin ihtiyaç duyduğu değerler uzantı alanlarında daha güvenilir biçimde taşınır. Örnekteki bakiye bu yüzden hem metinde hem de ayrı bir bakiye alanında yer alıyor.
instance
Problemin bu belirli oluşumunu tanımlayan bir URI başvurusudur. Erişilebilir bir adresse oradan problem ayrıntıları alınabilir; erişilebilir değilse sunucu için anlamlı ama istemci için opak bir tanımlayıcı görevi görür. Destek ekibine iletilen bir olay numarası gibi düşünebilirsiniz. Göreli URI uyarısı burada da geçerlidir. İncelediğimiz rehberin örneği instance olarak istek yolunu (/users) kullanır; bu yol olayı değil uç noktayı tanımladığı için, aynı uç noktadaki iki ayrı hatayı birbirinden ayırmaz. Olaya özgü bir kimlik daha çok bilgi verir.
| Üye | Tip | Anlamı | Önemli kural |
|---|---|---|---|
| type | URI başvurusu (metin) | Problem türü | Yoksa about:blank; birincil tanımlayıcı |
| title | Metin | Türün kısa adı | Olaydan olaya değişmez (yerelleştirme hariç) |
| status | Sayı | HTTP durum kodu | Gerçek yanıt koduyla aynı olmalı |
| detail | Metin | Bu olaya özgü açıklama | İstemci ayrıştırmamalı |
| instance | URI başvurusu (metin) | Bu olayın kimliği | Erişilebilir ya da opak olabilir |
Uzantı alanları ve çoklu hatalar
Problem türü tanımları nesneye türe özel ek üyeler ekleyebilir (MAY). İstemciler tanımadıkları uzantıları yok saymak zorundadır (MUST); bu kural problem türlerinin zamanla yeni bilgi ekleyerek gelişebilmesini sağlar. Uzantı adlarının harfle başlaması, yalnızca harf, rakam ve alt çizgiden oluşması ve en az üç karakter uzunluğunda olması önerilir (SHOULD); böylece XML gibi başka biçimlere de aktarılabilirler.
Doğrulama hataları bu mekanizmanın en yaygın kullanımıdır. RFC'deki örnek, her biri bir detail ve gövdedeki yeri gösteren bir JSON Pointer (pointer) taşıyan elemanlardan oluşan bir errors dizisi tanımlar:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problemler/dogrulama-hatasi",
"title": "İstek gövdesi geçerli değil.",
"status": 422,
"errors": [
{"detail": "en az 2 karakter olmalı", "pointer": "#/ad"},
{"detail": "admin, editor veya okur olmalı", "pointer": "#/rol"}
]
}İncelediğimiz rehber errors alanını farklı biçimde, alan adından mesaj listesine giden bir nesne olarak gösterir ({"email": ["..."]}). İki biçim de geçerli birer uzantıdır, çünkü standart errors diye bir üye tanımlamaz; RFC'deki dizi yalnızca bir örnektir. Dizi biçiminin avantajı, iç içe nesnelerdeki ve dizilerdeki konumları da pointer ile kesin olarak gösterebilmesidir.
Birbirinden farklı türde birden çok problem aynı anda oluştuğunda RFC, en ilgili ya da en acil olanın yanıtta gösterilmesini önerir (RECOMMENDED). Farklı türleri bir arada taşıyan genel "toplu" problem türleri oluşturmak mümkündür, ama bunlar HTTP anlamına iyi oturmaz: tek bir yanıtın tek bir durum kodu vardır.
about:blank ve yeni problem türü tanımlamak
about:blank, RFC 9457'nin kayda geçirdiği tek problem türüdür ve problemin durum kodunun anlamı dışında ek bir anlam taşımadığını söyler. Bu tür kullanıldığında title değerinin, o kodun önerilen durum ifadesiyle aynı olması önerilir (404 için "Not Found" gibi); istemcinin dil tercihine göre yerelleştirilebilir. type üyesi hiç gönderilmeyen her nesne zaten bu türü kullanmış sayılır:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "42 numaralı sipariş bulunamadı."
}Kendi türünüzü tanımladığınızda belgelemeniz gereken üç şey vardır (MUST): tür URI'si (genellikle https şemalı), kısa ve uygun bir başlık, birlikte kullanılacağı HTTP durum kodu. Tür tanımı uygun durumlarda Retry-After başlığının kullanımını belirtebilir; tür URI'sinin, problemin nasıl çözüleceğini anlatan bir HTML belgesine gitmesi önerilir. IANA'da "HTTP Problem Types" adlı bir kayıt bulunur; ancak satıcıya, uygulamaya ya da kuruluma özgü türler bu kayda alınamaz. Kendi API'nizin türleri, kontrolünüzdeki ve zaman içinde değişmeyen URI'ler altında yaşamalıdır.
RFC her şey için yeni tür tanımlamayı da önermez. Herhangi bir kaynakta görülebilecek gerçekten genel problemler, düz durum kodlarıyla daha iyi anlatılır: bir PUT isteğine verilen 403 Forbidden kendi başına yeterince açıktır, ayrıca "yazma izni yok" türü tanımlamaya gerek yoktur. Uygulamanızın hatayı taşıyabilecek kendi alana özgü bir biçimi zaten varsa, Problem Details onun yerine geçmek için değil, yeni bir hata biçimi icat etme ihtiyacını ortadan kaldırmak için vardır.
FastAPI ile Problem Details
Önce sık görülen bir hatayı görelim. FastAPI'nin HTTPException sınıfına detail olarak bir sözlük verdiğinizde, sözlük bir detail anahtarının içine konur ve yanıt türü application/json olarak kalır. İncelediğimiz rehberin örnek kodu da bu kalıbı kullanır; sonuç RFC 9457 yapısına uymaz:
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
app = FastAPI()
@app.post("/siparisler")
def siparis_ver():
# Yaygın hata: Problem Details nesnesini HTTPException detail alanına koymak
raise HTTPException(status_code=403, detail={
"type": "https://api.example.com/problemler/yetersiz-bakiye",
"title": "Bakiyeniz yetersiz.",
"status": 403,
})
r = TestClient(app).post("/siparisler", json={"urun_id": 123456, "adet": 2})
print(r.status_code, r.headers["content-type"])
# çıktı: 403 application/json
print(r.json())
# çıktı: {'detail': {'type': 'https://api.example.com/problemler/yetersiz-bakiye', 'title': 'Bakiyeniz yetersiz.', 'status': 403}}Doğru yol, kendi istisna sınıfınızı ve onun için bir istisna işleyicisi (exception handler) tanımlayıp yanıtı JSONResponse ile, media_type="application/problem+json" vererek üretmektir. Aşağıdaki örnek aynı işleyiciyi Pydantic doğrulama hataları için de kullanıyor ve her hatanın gövdedeki yerini RFC örneğindeki gibi pointer olarak yazıyor. Olay kimliği burada çıktının sabit olması için bir sayaçla üretiliyor; gerçek uygulamada her olay için benzersiz bir kimlik (örneğin UUID) üretip günlüğe de aynı kimlikle yazmak, destek talebini olayla eşleştirmeyi kolaylaştırır:
import itertools
from typing import Literal
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
TIP = "https://api.example.com/problemler/"
sayac = itertools.count(1) # gerçekte her olay için benzersiz bir kimlik (örneğin UUID)
class Problem(Exception):
def __init__(self, status: int, tip: str, title: str, detail: str | None = None, **uzanti):
self.status, self.tip, self.title, self.detail, self.uzanti = status, tip, title, detail, uzanti
def problem_yaniti(status, tip, title, detail=None, **uzanti):
govde = {"type": tip, "title": title, "status": status}
if detail:
govde["detail"] = detail
govde["instance"] = f"/olaylar/{next(sayac)}"
govde.update(uzanti)
return JSONResponse(status_code=status, content=govde, media_type="application/problem+json")
@app.exception_handler(Problem)
async def problem_isleyici(request: Request, exc: Problem):
return problem_yaniti(exc.status, exc.tip, exc.title, exc.detail, **exc.uzanti)
@app.exception_handler(RequestValidationError)
async def dogrulama_isleyici(request: Request, exc: RequestValidationError):
hatalar = [{"detail": e["msg"], "pointer": "#/" + "/".join(str(x) for x in e["loc"][1:])}
for e in exc.errors() if e["loc"] and e["loc"][0] == "body"]
return problem_yaniti(422, TIP + "dogrulama-hatasi", "İstek gövdesi geçerli değil.", errors=hatalar)
class Siparis(BaseModel):
urun_id: int
adet: int = Field(ge=1, le=10)
teslimat: Literal["standart", "hızlı"] = "standart"
BAKIYE = 30 # gerçekte hesap servisinden gelir
@app.post("/siparisler", status_code=201)
def siparis_ver(s: Siparis):
tutar = 25 * s.adet
if tutar > BAKIYE:
raise Problem(403, TIP + "yetersiz-bakiye", "Bakiyeniz yetersiz.",
f"Mevcut bakiyeniz {BAKIYE} TL, bu sipariş {tutar} TL tutuyor.", bakiye=BAKIYE)
return {"id": 1, **s.model_dump()}
c = TestClient(app)
r = c.post("/siparisler", json={"urun_id": 123456, "adet": 2})
print(r.status_code, r.headers["content-type"])
# çıktı: 403 application/problem+json
print(r.json())
# çıktı: {'type': 'https://api.example.com/problemler/yetersiz-bakiye', 'title': 'Bakiyeniz yetersiz.', 'status': 403, 'detail': 'Mevcut bakiyeniz 30 TL, bu sipariş 50 TL tutuyor.', 'instance': '/olaylar/1', 'bakiye': 30}
r = c.post("/siparisler", json={"urun_id": 123456, "adet": 0, "teslimat": "uçak"})
print(r.status_code, r.json()["errors"])
# çıktı: 422 [{'detail': 'Input should be greater than or equal to 1', 'pointer': '#/adet'}, {'detail': "Input should be 'standart' or 'hızlı'", 'pointer': '#/teslimat'}]
r = c.post("/siparisler", json={"urun_id": 123456, "adet": 1})
print(r.status_code, r.json())
# çıktı: 201 {'id': 1, 'urun_id': 123456, 'adet': 1, 'teslimat': 'standart'}İlk yanıtta durum kodu ile status aynı, yanıt türü application/problem+json ve makinenin ihtiyaç duyduğu bakiye ayrı bir alanda. İkinci yanıtta iki doğrulama hatası tek bir 422 içinde, her biri kendi konumuyla raporlanıyor. Bu işleyici yalnızca gövde hatalarını pointer ile gösteriyor; sorgu parametresi hataları için ayrı bir uzantı alanı tanımlayabilirsiniz. Pydantic mesajları İngilizce geldiği için Türkçe bir arayüzde detail metinlerini type değerine göre çevirmek de mümkündür. Hangi durumda 400, hangisinde 422 kullanılacağı 400 ve 422 farkı yazısında ayrıntılı anlatılıyor.
Güvenlik: hata yanıtında ne olmamalı?
RFC 9457, Problem Details'ın uygulamanın iç yapısı için bir hata ayıklama aracı olmadığını, HTTP arayüzünün kendisi hakkında ayrıntı vermenin bir yolu olduğunu vurgular. Yeni bir tür tanımlarken de gerçek bir problem üretirken de verilen bilgi dikkatle süzülmelidir: sistemi ele geçirmeye, sisteme erişmeye ya da kullanıcıların gizliliğini ihlal etmeye yarayabilecek bilgi sızdırma riski vardır. Yığın dökümü (stack trace) gibi uygulama ayrıntılarını HTTP arayüzünden sunmaktan kaçınılması özellikle önerilir. Bir başka not da status üyesiyle ilgilidir: gerçek durum koduyla çelişebilir ve proxy, yük dengeleyici, güvenlik duvarı gibi genel yazılımlar bu üyeyi bilmez ve dikkate almaz. Asıl bilgi her zaman HTTP durum kodudur. Genel API güvenliği için REST API güvenlik rehberi yazısına bakabilirsiniz.
Sık yapılan hatalar

- Gövdedeki status ile yanıt kodunun farklı olması: 200 yanıtın içinde
"status": 404göndermek RFC'nin MUST kuralını çiğner ve ara katmanları yanıltır. - Yanlış medya türü: Problem Details gövdesini
application/jsonile göndermek çalışır ama istemcinin yanıtı türünden tanımasını engeller. - title alanını olaya göre değiştirmek: olaya özgü bilgi
detailiçine gider;titletür için sabittir. - İstemcide detail metnini ayrıştırmak: metin her an değişebilir. Gereken değerleri uzantı alanı olarak tanımlayın.
- Göreli ve kısa tür URI'leri:
"type": "hata"gibi bir değer, farklı uç noktalarda farklı adreslere çözümlenir. Mutlak URI kullanın. - Her genel durum için yeni tür: durum kodunun kendisinin yeterli olduğu yerde
about:blankkullanın. - Yığın dökümü sızdırmak: 500 hatalarında bile iç ayrıntıları gövdeye koymayın; onları olay kimliğiyle günlüğe yazın.
Sık Sorulan Sorular
RFC 7807 kullanan bir API'yi değiştirmem gerekir mi?
Nesnenin üyeleri ve medya türü aynı kaldığı için RFC 7807'ye göre yazılmış yanıtlar RFC 9457 ile de uyumludur. Yeni belge kayıt, çoklu problemler ve çözümlenemeyen URI'ler konusunda açıklama ekler; atıflarınızı RFC 9457'ye güncellemeniz yeterlidir.
type alanındaki URI gerçekten açılabilir olmalı mı?
Zorunlu değil, ama önerilir. Açılabilir bir URI, hatayla karşılaşan geliştiricinin çözüm belgesine ulaşmasını sağlar ve ileride tür URI'sini değiştirme zorunluluğunu ortadan kaldırır.
XML kullanan bir API Problem Details kullanabilir mi?
Evet. RFC 9457 ekinde application/problem+xml medya türüyle eşdeğer bir XML biçimi tanımlanır. JSON tabanlı bir API'de JSON biçimi doğal tercihtir.
Başarılı yanıtlarda da kullanılır mı?
Problem Details her durum koduyla kullanılabilir, ama en doğal olarak 4xx ve 5xx yanıtlarına uyar. Yanıt hâlâ bir kaynağın temsiliyse RFC, ayrıntıları o uygulamanın kendi biçiminde anlatmanın çoğu zaman daha uygun olduğunu söyler.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: REST API tasarımı öğrenme rehberi · Hata mesajı formatı ve hata kodları · 400 ve 422 farkı · Durum kodu seçimi · 401 ve 403 farkı · HTTP durum kodları