Kısa cevap: istek gövdesi JSON olarak ayrıştırılamıyorsa (bozuk sözdizimi) 400 Bad Request, gövde geçerli JSON olduğu hâlde içerik kurallarınıza uymuyorsa (eksik alan, yanlış tip, izin verilmeyen değer) 422 Unprocessable Content, gövde hiç desteklemediğiniz bir biçimdeyse 415 Unsupported Media Type döndürmek RFC 9110'daki tanımlarla en uyumlu ayrımdır. FastAPI ise varsayılan olarak bu durumların hepsine 422 ile cevap verir; bu yazıda bunun nedenini, gerçek yanıtını ve isterseniz nasıl ayıracağınızı gösteriyoruz.

Doğrulama (validation) tek bir adım değil, katmanlı bir süreçtir: önce gövdenin türüne, sonra sözdizimine, sonra şemaya, en son da iş kurallarına bakılır. Her katmanın başarısızlığı farklı bir durum koduna karşılık gelir. Durum kodlarının genel listesi için HTTP durum kodları, senaryoya göre kod seçimi için REST API durum kodu seçimi yazısına bakabilirsiniz; burada yalnızca istek doğrulamasını ele alıyoruz.

İstek gövdesi ve Content-Type

POST, PUT ve PATCH isteklerinde istemci gövdenin türünü Content-Type başlığıyla bildirir. İncelediğimiz tasarım rehberinin istek örnekleri JSON gövdeyi Content-Type: application/json ile gönderir:

http
POST /kullanicilar HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "email": "zeynep@example.com",
  "ad": "Zeynep Demir",
  "rol": "editor"
}

RFC 9110'a göre içerik taşıyan bir mesajı üreten taraf Content-Type göndermelidir (SHOULD). Başlık yoksa alıcı gövdeyi application/octet-stream sayabilir ya da içeriğe bakarak türünü tahmin edebilir. RFC aynı bölümde içerik tahmininin (MIME sniffing) yanlış sonuçlara ve güvenlik risklerine yol açabileceğini de not eder. API tarafında bunun anlamı şudur: hangi türleri kabul ettiğinizi belgeleyin ve başka bir türle gelen gövdeyi sessizce yorumlamaya çalışmayın.

Accept başlığı ise istemcinin yanıt için hangi türü istediğini söyler; o konu, yani içerik anlaşması (content negotiation), Accept başlığı ve content negotiation yazısında anlatılıyor.

400, 415 ve 422: RFC 9110 ne diyor?

400, 415 ve 422: RFC 9110 ne diyor? 415 Unsupported Media Type, 400 Bad Request, 422 Unprocessable Content

Üç kodun tanımı birbirini tamamlar ve aralarındaki sınır, sunucunun gövdeyi ne kadar anlayabildiğine göre çizilir:

  • 415 Unsupported Media Type: sunucu, içeriğin biçimi bu metot ve hedef kaynak için desteklenmediği için isteği reddeder. Sorun Content-Type ya da Content-Encoding değerinden veya verinin doğrudan incelenmesinden kaynaklanabilir. Neden desteklenmeyen bir medya türüyse, yanıttaki Accept başlığı hangi türlerin kabul edileceğini gösterebilir.
  • 400 Bad Request: sunucu, istemci hatası olarak algıladığı bir sebeple isteği işleyemez ya da işlemez. RFC'nin verdiği örnekler bozuk istek sözdizimi, geçersiz mesaj çerçevesi ve aldatıcı yönlendirmedir. Ayrıştırılamayan bir JSON gövdesi bu tanıma doğrudan uyar.
  • 422 Unprocessable Content: sunucu içeriğin türünü anlar (bu yüzden 415 uygun değildir) ve sözdizimi doğrudur, ama içerdiği talimatları işleyemez. Bu kod daha önce WebDAV belirtiminde (RFC 4918) Unprocessable Entity adıyla tanımlıydı; RFC 9110 genel olarak uygulanabilir olduğu için onu HTTP'nin ortak kodları arasına aldı.

Bu tanımlardan pratik bir karar sırası çıkar. Gövde türü yanlışsa 415; tür doğru ama metin JSON olarak ayrıştırılamıyorsa 400; JSON ayrıştırıldı ama zorunlu alan eksik, tip yanlış ya da değer aralık dışındaysa 422. İncelediğimiz rehber de 400'ü "geçersiz istek gövdesi veya parametre", 422'yi "doğrulama hatası" olarak ayırır. 400 genel bir istemci hatası kodu olduğu için bazı API'ler şema hatalarında da 400 kullanır; bu da savunulabilir bir seçimdir. Önemli olan, seçiminizi belgelemek ve her uç noktada (endpoint) aynı şekilde uygulamaktır.

Dördüncü katman: iş kuralları

Dördüncü katman: iş kuralları

Şemaya uyan bir istek yine de reddedilebilir. Kayıtlı bir e-posta adresiyle ikinci bir kullanıcı oluşturulmaya çalışılması buna örnektir: gövde kusursuzdur, sorun kaynağın mevcut durumuyla çakışmasıdır. RFC 9110 bu durumu 409 Conflict ile tanımlar: istek, hedef kaynağın mevcut durumuyla çakıştığı için tamamlanamamıştır ve kullanıcı çakışmayı çözüp isteği yeniden gönderebilir. İncelediğimiz rehber de 409 için yinelenen e-posta örneğini verir. Böylece istemci, "gönderdiğim veri yanlış" (422) ile "veri doğru ama şu an kabul edilemez" (409) arasındaki farkı koddan anlayabilir.

Aynı mantıkla gövde çok büyükse 413 Content Too Large uygundur; RFC 9110 bu kodu sunucunun işlemeye istekli ya da yetenekli olduğundan büyük içerik için tanımlar. Yetkisi olmayan bir kullanıcının isteği ise doğrulama sorunu değildir; 401 ve 403 farkı yazısına bakın.

Pydantic ile şema doğrulaması

FastAPI'de gövde şeması bir Pydantic modeliyle tanımlanır; uç nokta fonksiyonu modeli parametre olarak aldığında FastAPI gövdeyi ayrıştırır, modele göre doğrular ve başarısız olursa fonksiyonu hiç çağırmadan hata yanıtı üretir. Aşağıdaki örnekte e-posta için bir uzunluk sınırı ve basit bir desen, ad için uzunluk sınırları, rol için ise yalnızca üç değere izin veren bir Literal tanımlı. extra="forbid" ayarı, modelde olmayan bir alan gönderildiğinde isteği reddeder. Veritabanı yerine bellekteki bir küme kullanılıyor; gerçek uygulamada benzersizlik veritabanında denetlenir:

python
from typing import Literal
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict, Field

app = FastAPI()
KAYITLI = {"ayse@example.com"}  # gerçekte veritabanı

class KullaniciOlustur(BaseModel):
    model_config = ConfigDict(extra="forbid")  # tanımsız alan gelirse reddet

    email: str = Field(max_length=254, pattern=r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
    ad: str = Field(min_length=2, max_length=100)
    rol: Literal["admin", "editor", "okur"] = "okur"

@app.post("/kullanicilar", status_code=201)
def olustur(k: KullaniciOlustur):
    if k.email in KAYITLI:  # iş kuralı: şema geçerli ama durum çakışıyor
        raise HTTPException(status_code=409, detail="Bu e-posta zaten kayıtlı")
    KAYITLI.add(k.email)
    return {"id": 43, **k.model_dump()}

c = TestClient(app)

def goster(r):
    if r.status_code == 422:
        print(r.status_code, [(e["type"], e["loc"]) for e in r.json()["detail"]])
    else:
        print(r.status_code, r.json())

goster(c.post("/kullanicilar", json={"email": "zeynep@example.com", "ad": "Zeynep"}))
# çıktı: 201 {'id': 43, 'email': 'zeynep@example.com', 'ad': 'Zeynep', 'rol': 'okur'}
goster(c.post("/kullanicilar", json={"ad": "Z", "rol": "patron"}))
# çıktı: 422 [('missing', ['body', 'email']), ('string_too_short', ['body', 'ad']), ('literal_error', ['body', 'rol'])]
goster(c.post("/kullanicilar", json={"email": "can@example.com", "ad": "Can", "yetki": "tam"}))
# çıktı: 422 [('extra_forbidden', ['body', 'yetki'])]
goster(c.post("/kullanicilar", content='{"email": "can@example.com",',
              headers={"Content-Type": "application/json"}))
# çıktı: 422 [('json_invalid', ['body', 28])]
goster(c.post("/kullanicilar", content="email=can@example.com&ad=Can",
              headers={"Content-Type": "application/x-www-form-urlencoded"}))
# çıktı: 422 [('model_attributes_type', ['body'])]
goster(c.post("/kullanicilar", json={"email": "ayse@example.com", "ad": "Ayşe"}))
# çıktı: 409 {'detail': 'Bu e-posta zaten kayıtlı'}

Çıktıları sırayla okuyalım. Geçerli istek 201 ile dönüyor ve gönderilmeyen rol varsayılan değeri alıyor. İkinci istekte üç hata tek seferde raporlanıyor: Pydantic ilk hatada durmaz, tüm alanları denetler. Bu, istemcinin formu tek turda düzeltebilmesi açısından önemlidir. Üçüncü istekte yetki adında tanımsız bir alan var ve extra="forbid" sayesinde reddediliyor. Bu ayar olmasaydı Pydantic'in varsayılan davranışı tanımsız alanı yok saymaktır: istemci alan adını yanlış yazdığında (role yerine rol gibi) hata almaz, değer sessizce kaybolur.

Dördüncü ve beşinci çıktılar FastAPI'nin varsayılan davranışını gösteriyor: bozuk JSON da (json_invalid, konum olarak hatanın bulunduğu karakter sırası) form biçiminde gönderilmiş gövde de (model_attributes_type) 422 alıyor. Yani FastAPI, ayrıştırma ve tür hatalarını da doğrulama hatası sınıfına koyar. Son satırda ise şema geçerli ama e-posta kayıtlı olduğu için iş kuralı 409 döndürüyor.

FastAPI'nin varsayılan 422 yanıtı

FastAPI'nin doğrulama hatası gövdesi bir detail dizisidir. Her eleman bir hatayı anlatır: type makine için hata türü, loc hatanın yeri, msg insan için açıklama, input gelen değer, ctx ise varsa kuralın parametreleridir. Tek alanlı bir hatanın tam gövdesi şöyle:

python
import json
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

app = FastAPI()

class KullaniciOlustur(BaseModel):
    email: str
    ad: str = Field(min_length=2)

@app.post("/kullanicilar", status_code=201)
def olustur(k: KullaniciOlustur):
    return k

r = TestClient(app).post("/kullanicilar", json={"email": "can@example.com", "ad": "C"})
print(r.status_code, r.headers["content-type"])
# çıktı: 422 application/json
print(json.dumps(r.json(), ensure_ascii=False))
# çıktı: {"detail": [{"type": "string_too_short", "loc": ["body", "ad"], "msg": "String should have at least 2 characters", "input": "C", "ctx": {"min_length": 2}}]}

loc dizisinin ilk elemanı değerin nereden geldiğini söyler: body gövde, query sorgu parametresi, path yol parametresi. Örneğin limit sorgu parametresine izin verilen aralığın dışında bir sayı gönderildiğinde konum ["query", "limit"] olur; path ve query ayrımı path ve query parametresi yazısında anlatılıyor. İçteki alanlar için dizi uzar: iç içe bir nesnede ["body", "adres", "il"] gibi.

Bu gövdede dikkat edilmesi gereken bir alan var: input, istemcinin gönderdiği değeri yanıtta geri yansıtır. Kısa bir parola gönderildiğinde o parola hata yanıtında da yer alır ve yanıtları kaydeden bir günlük (log) ya da ara katman onu saklayabilir. Hassas alanlarınız varsa hata gövdesini kendiniz biçimlendirip input değerini çıkarmayı düşünün. Kendi biçiminizi kurmak için Problem Details ve hata mesajı formatı yazılarında hazır kalıplar var.

400, 415 ve 422'yi FastAPI'de ayırmak

Varsayılan davranışı değiştirmek için RequestValidationError için bir istisna işleyicisi (exception handler) tanımlanır. Aşağıdaki işleyici önce gövde türüne bakar ve JSON dışı bir türde 415 ile birlikte kabul edilen türü Accept başlığında bildirir; ardından hata listesinde json_invalid varsa 400 döndürür; kalan her şeyi 422 olarak bırakır:

python
from fastapi import FastAPI, Request
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

app = FastAPI()

class KullaniciOlustur(BaseModel):
    email: str
    ad: str = Field(min_length=2)

@app.exception_handler(RequestValidationError)
async def dogrulama_hatasi(request: Request, exc: RequestValidationError):
    tur = request.headers.get("content-type", "").split(";")[0].strip().lower()
    if request.method in ("POST", "PUT", "PATCH") and tur not in ("", "application/json"):
        return JSONResponse(status_code=415, headers={"Accept": "application/json"},
                            content={"detail": "Gövde application/json olmalı"})
    if any(e["type"] == "json_invalid" for e in exc.errors()):
        return JSONResponse(status_code=400, content={"detail": "Gövde geçerli bir JSON değil"})
    return JSONResponse(status_code=422, content={"detail": jsonable_encoder(exc.errors())})

@app.post("/kullanicilar", status_code=201)
def olustur(k: KullaniciOlustur):
    return k

c = TestClient(app)
r = c.post("/kullanicilar", content='{"email": ', headers={"Content-Type": "application/json"})
print(r.status_code, r.json())
# çıktı: 400 {'detail': 'Gövde geçerli bir JSON değil'}
r = c.post("/kullanicilar", content="email=a", headers={"Content-Type": "text/plain"})
print(r.status_code, r.headers["accept"], r.json())
# çıktı: 415 application/json {'detail': 'Gövde application/json olmalı'}
r = c.post("/kullanicilar", json={"email": "can@example.com", "ad": "C"})
print(r.status_code, r.json()["detail"][0]["msg"])
# çıktı: 422 String should have at least 2 characters
r = c.post("/kullanicilar", json={"email": "can@example.com", "ad": "Can"})
print(r.status_code, r.json())
# çıktı: 201 {'email': 'can@example.com', 'ad': 'Can'}

İşleyicide Content-Type değerinin ; işaretinden önceki kısmı küçük harfe çevrilerek karşılaştırılıyor, çünkü RFC 9110'a göre tür ve alt tür büyük-küçük harfe duyarsızdır ve başlık charset gibi parametreler taşıyabilir. jsonable_encoder ise hata listesindeki her değerin JSON'a yazılabilir olmasını sağlar. Dikkat edilecek bir ayrıntı: denediğimiz sürümde FastAPI, Content-Type başlığı hiç gönderilmeyen gövdeyi JSON olarak ayrıştırmayı deniyor; bu yüzden işleyici boş türü de kabul ediyor. Başlığı zorunlu tutmak isterseniz bu koşulu kaldırabilirsiniz.

Hangi durumda hangi kod?

Hangi durumda hangi kod? Desteklenmeyen gövde türü, Ayrıştırılamayan JSON, Zorunlu alan eksik, Yanlış tip ya da aralık dışı
DurumÖrnekÖnerilen kod
Desteklenmeyen gövde türüContent-Type: text/plain415 Unsupported Media Type
Ayrıştırılamayan JSONKapanmamış süslü parantez400 Bad Request
Zorunlu alan eksikemail yok422 Unprocessable Content
Yanlış tip ya da aralık dışıyas: "on", limit: 1000422 Unprocessable Content
Tanımsız alan (forbid ile)yetki: "tam"422 Unprocessable Content
Mevcut durumla çakışmaKayıtlı e-posta409 Conflict
Gövde çok büyükSınırın üstünde dosya413 Content Too Large
Geçerli istek, kaynak oluştuYeni kullanıcı201 Created

Sık yapılan hatalar

  • Doğrulamayı yalnızca istemciye bırakmak: tarayıcıdaki form denetimi kullanıcı deneyimi içindir. API'ye doğrudan istek gönderen biri onu atlar; asıl doğrulama sunucuda yapılmalıdır. İncelediğimiz rehberin yayın öncesi kontrol listesinde istek gövdesi doğrulaması ayrı bir maddedir.
  • Geçersiz girdide 500 döndürmek: eksik bir anahtara erişen kod KeyError ile çökerse istemci, kendi hatası olan bir durumu sunucu hatası olarak görür. Girdiyi şemayla doğrulamak bu tür çökmeleri baştan engeller.
  • Hata için 200 döndürmek: {"ok": false} gövdesiyle 200 göndermek, durum koduna bakan istemci kütüphanelerini ve ara katmanları yanıltır.
  • Tanımsız alanları sessizce kabul etmek: yanlış yazılmış alan adı fark edilmeden kaybolur. En azından yazma uç noktalarında extra="forbid" düşünün.
  • Hataları birer birer bildirmek: ilk hatada durup yalnızca onu raporlamak, istemciyi her alan için ayrı bir deneme yapmaya zorlar.
  • İş kuralı ihlaline 422 vermek: yinelenen kayıt gibi durumsal çakışmalarda 409 daha açıklayıcıdır; istemci veriyi değil durumu düzeltmesi gerektiğini anlar.

Sık Sorulan Sorular

Tüm doğrulama hatalarında 400 kullansam olur mu?

Olur; 400 genel istemci hatası kodudur ve RFC 9110 tanımı bunu kapsar. 422 ise "anladım ama işleyemiyorum" anlamını daha dar ve kesin anlatır. Hangisini seçerseniz seçin, belgeleyin ve tüm uç noktalarda aynı kalın.

FastAPI neden bozuk JSON için de 422 veriyor?

FastAPI, gövdeyi ayrıştırma ve şemaya uydurma adımlarını tek bir doğrulama süreci olarak ele alır ve tüm başarısızlıkları RequestValidationError olarak toplar; varsayılan işleyici bunların hepsini 422 ile döndürür. Ayrım isterseniz yukarıdaki gibi kendi işleyicinizi yazabilirsiniz.

Sorgu parametresi hataları da 422 mi?

FastAPI'de evet: Query(ge=1, le=100) gibi bir kısıtı ihlal eden değer, konumu query ile başlayan bir 422 hatası üretir. Filtre ve sıralama parametrelerinde izin verilen alanların nasıl sınırlandığı filtreleme ve sıralama yazısında anlatılıyor.

Hata mesajlarını Türkçe döndürebilir miyim?

Pydantic'in msg metinleri İngilizcedir. Kendi işleyicinizde type değerine göre Türkçe bir açıklama üretebilirsiniz; istemcinin ise metni değil type ya da kendi hata kodunuz gibi sabit, makine okunur alanları kullanması gerekir. Metin sonradan değişebilir, kod değişmemelidir.

415 yanıtına Accept başlığı eklemek zorunlu mu?

Zorunlu değil. RFC 9110, neden desteklenmeyen bir medya türü olduğunda yanıttaki Accept başlığının kabul edilen türleri göstermek için kullanılabileceğini söyler. Eklemek, istemcinin isteğini nasıl düzelteceğini anlamasını kolaylaştırır.

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 · Problem Details hata yanıtı · Hata mesajı formatı · Durum kodu seçimi · JSON alan adlandırma · REST API güvenlik rehberi