HTTP Method ve Status Code Semantiği: Safe, Idempotent, PUT mu PATCH mı, 200/201/202/204/409/412/422 Kararları
Method ve status code seçimi, RFC 9110'daki safe ve idempotent tanımlarına ve ekipçe bir kez yazılmış bir karar kuralına dayanmalı: 400 bozuk istek, 422 iş kuralının reddettiği geçerli istek, 409 resource'un durumuyla çakışma, 412 If-Match uyuşmazlığı. Bookstore'da POST ile 201 ve Location'ı, PUT ile oluşturmayı, JSON Merge Patch ile 415 ve Accept-Patch'i, 401, 403, 404 ve 405 ayrımını gerçek yanıtlarla görüyoruz.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 (ASP.NET Core 10.0.15) ile derlenip denenmiştir; HTTP örnekleri bu uygulamanın gerçek yanıtlarıdır, OpenAPI 3.2 dokümanı Redocly CLI 2.78.0 ve Spectral 6.17.1 ile lint edilmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) katalog ekibinden Ayşe, yeni gelen "The Count of Monte Cristo"yu sisteme ekliyor ve sonra fiyatını düzeltiyor. Aynı saatte müşteri Ada da sepetine kitap koyuyor. Aşağıdaki isteklerin her biri için sunucunun hangi status code'u döneceğini tahmin et:
# 1. Ayşe aynı kitabı ikinci kez ekliyor
curl -X POST -H "Authorization: Bearer ayse" -H "Content-Type: application/json" -d '{"isbn":"9780140449266","title":"The Count of Monte Cristo","author":"Alexandre Dumas","category":"classics","price":{"amount":399.00,"currency":"TRY"}}' http://127.0.0.1:52318/books
# 2. Angular'daki form fiyatı düz JSON ile gönderiyor
curl -X PATCH -H "Authorization: Bearer ayse" -H "Content-Type: application/json" -d '{"price":{"amount":379.00}}' http://127.0.0.1:52318/books/9780140449266
# 3. Ada aynı sepet satırını iki kez gönderiyor (ikinci istek)
curl -X PUT -H "Authorization: Bearer ada" -H "Content-Type: application/json" -d '{"quantity":2}' http://127.0.0.1:52318/customers/me/cart/items/9780441569595
# 4. Ada stokta 2 tane olan kitaptan 5 tane istiyor
curl -X PUT -H "Authorization: Bearer ada" -H "Content-Type: application/json" -d '{"quantity":5}' http://127.0.0.1:52318/customers/me/cart/items/9780441569595
# 5. Ayşe bir kitabı ikinci kez siliyor
curl -X DELETE -H "Authorization: Bearer ayse" http://127.0.0.1:52318/books/9780140444308
Her isteğin en az iki makul cevabı var. Bu yazının amacı, hangisinin seçileceğini her endpoint'te yeniden tartışmamak için kuralı bir kez yazmak.
Safe ve Idempotent: RFC 9110 Ne Söylüyor?
HTTP method'larının anlamı, IETF'in (internet standartlarını geliştiren kuruluş) RFC'lerinden (Request for Comments; numaralı standart dokümanları) 9110 numaralı HTTP Semantics'te tanımlı ve iki özellik tasarımın temelini oluşturuyor. Safe method, istemcinin sunucuda bir değişiklik istemediği method'dur: GET, HEAD, OPTIONS ve TRACE (isteği olduğu gibi geri yansıtan teşhis method'u). Idempotent method ise aynı isteği bir kez de göndersen on kez de göndersen sunucuda amaçlanan etkinin aynı kaldığı method'dur: PUT, DELETE ve bütün safe method'lar. Haziran 2026'da yayımlanan RFC 10008 listeye QUERY'yi (body taşıyabilen, ama safe ve idempotent olan yeni sorgu method'u) ekledi. OpenAPI 3.2 de query operation'ını tanıyor; arama endpoint'lerinde bunu 05. yazıda tartışacağız.
| Method | Safe | Idempotent | Bookstore'daki kullanımı |
|---|---|---|---|
| GET, HEAD | Evet | Evet | Kitap, sipariş, sepet okumak; HEAD ile yalnız ETag almak |
| PUT | Hayır | Evet | Kitabı ISBN (kitabın uluslararası standart numarası) ile oluşturmak ya da tamamen değiştirmek, sepet satırının adedini set etmek |
| DELETE | Hayır | Evet | Kitabı katalogdan, satırı sepetten çıkarmak |
| POST | Hayır | Hayır | Kitap eklemek, iade talebi, :cancel gibi custom method'lar |
| PATCH | Hayır | Hayır (garanti yok) | Kitabın bazı alanlarını değiştirmek |
| QUERY | Evet | Evet | Henüz yok; body'li arama için aday (RFC 10008) |
Idempotency etkiyle ilgilidir, yanıtla değil. Bir kitabı iki kez silmek sunucuyu aynı durumda bırakır (kitap yok), ama ikinci yanıt 404'tür. Bu yüzden 5. sorunun cevabı 404, ve bu idempotency'yi bozmaz. Pratik önemi şurada: bağlantı koptuğunda istemci, reverse proxy ya da HTTP library'si idempotent bir isteği gönül rahatlığıyla tekrar gönderebilir. POST'u tekrar göndermek ise ikinci bir sipariş demek olabilir; bu problemin contract tarafı Idempotency-Key header'ıyla 07. yazıda, implementasyonu Çözüm Desenleri serisinin "Idempotency Key" yazısında.
POST ile Oluşturmak: 201, Location ve 409
Bir collection'a POST, sunucunun yeni bir üye oluşturması demek. Başarılı yanıt 201'dir, Location header'ı yeni resource'un adresini verir ve body'de oluşan representation döner; istemci ikinci bir GET'e ihtiyaç duymaz:
$ curl -i -X POST -H "Authorization: Bearer ayse" -H "Content-Type: application/json" -d '{"isbn":"9780140449266","title":"The Count of Monte Cristo","author":"Alexandre Dumas","category":"classics","price":{"amount":399.00,"currency":"TRY"}}' http://127.0.0.1:52318/books
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Sat, 14 Nov 2026 22:37:01 GMT
Server: Kestrel
ETag: "1"
Location: /books/9780140449266
Transfer-Encoding: chunked
{"isbn":"9780140449266","title":"The Count of Monte Cristo","author":"Alexandre Dumas","category":"classics","price":{"amount":399.00,"currency":"TRY"},"inStock":false}
$ curl -s -X POST -H "Authorization: Bearer ayse" -H "Content-Type: application/json" -d '{"isbn":"9780140449266","title":"The Count of Monte Cristo","author":"Alexandre Dumas","category":"classics","price":{"amount":399.00,"currency":"TRY"}}' http://127.0.0.1:52318/books | jq -c '{status, title, detail}'
{"status":409,"title":"Book already exists","detail":"A book with ISBN 9780140449266 already exists. Use PUT /books/9780140449266 to replace it."}
İkinci istek 1. sorunun cevabı: 409. RFC 9110'a göre 409, isteğin hedef resource'un mevcut durumuyla çakıştığını ve kullanıcının çakışmayı çözüp tekrar deneyebileceğini söyler. Collection'da bu ISBN zaten var; çözüm de detail'de yazıyor. 400 dönseydik istemci isteğinde bir yazım hatası arardı, oysa istek kusursuz.
PUT: Tam Değiştirme ve İstemcinin Bildiği Kimlik
PUT, hedef URL'deki resource'un durumunu gönderilen representation ile değiştirmek demektir. Kimliği istemci biliyorsa (kitapta ISBN) PUT oluşturmak için de kullanılabilir: RFC 9110 yeni oluşan resource için 201, var olanın değişmesi için 200 ya da 204 ister. Aynı PUT'u iki kez göndermek bunu gösteriyor; ilki oluşturuyor, ikincisi aynı içerikle değiştiriyor:
$ curl -s -o /dev/null -w '%{http_code}\n' -X PUT -H "Authorization: Bearer ayse" -H "Content-Type: application/json" -d '{"title":"Les Miserables","author":"Victor Hugo","category":"classics","price":{"amount":529.00,"currency":"TRY"}}' http://127.0.0.1:52318/books/9780140444308
201
$ curl -s -o /dev/null -w '%{http_code}\n' -X PUT -H "Authorization: Bearer ayse" -H "Content-Type: application/json" -d '{"title":"Les Miserables","author":"Victor Hugo","category":"classics","price":{"amount":529.00,"currency":"TRY"}}' http://127.0.0.1:52318/books/9780140444308
200
$ curl -s -X PUT -H "Authorization: Bearer ayse" -H "Content-Type: application/json" -d '{"title":"Les Miserables","price":{"amount":499.00,"currency":"TRY"}}' http://127.0.0.1:52318/books/9780140444308 | jq -c '{status, errors}'
{"status":400,"errors":{"Author":["The Author field is required."],"Category":["The Category field is required."]}}
Üçüncü istek PUT'un en sık yanlış kullanımını gösteriyor: yalnız değişen alanları gönderip gerisinin korunmasını beklemek. PUT tam değiştirmedir; eksik alan "eskisi kalsın" değil, geçersiz istek demek. Azure rehberi de PUT'ta zorunlu bir alan eksikse 400 dönülmesini ister. Hata anahtarlarının (Author) JSON'daki alan adlarıyla (author) aynı biçimde olmadığını da fark etmiş olabilirsin; bu bir contract hatası ve 04. yazının konusu. Kısmi güncelleme için doğru araç PATCH.
Sepet satırında aynı tasarım 3. sorunun cevabını veriyor. PUT /customers/me/cart/items/{isbn} satırın adedini set ediyor, artırmıyor; ilk istek satırı oluşturuyor (201), ikinci aynı istek aynı sepeti bırakıyor (200). Ada çift tıklasa da sepetinde 4 değil 2 kitap olur. "Sepete bir tane daha ekle" gibi artıran bir POST tasarlasaydık çift tıklama gerçekten iki kitap eklerdi.
$ curl -i -X PUT -H "Authorization: Bearer ada" -H "Content-Type: application/json" -d '{"quantity":2}' http://127.0.0.1:52318/customers/me/cart/items/9780441569595
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Sat, 14 Nov 2026 22:37:01 GMT
Server: Kestrel
Location: /customers/me/cart/items/9780441569595
Transfer-Encoding: chunked
{"isbn":"9780441569595","title":"Neuromancer","quantity":2,"unitPrice":{"amount":279.00,"currency":"TRY"}}
$ curl -s -o /dev/null -w '%{http_code}\n' -X PUT -H "Authorization: Bearer ada" -H "Content-Type: application/json" -d '{"quantity":2}' http://127.0.0.1:52318/customers/me/cart/items/9780441569595
200
PATCH: JSON Merge Patch mi, JSON Patch mi?
PATCH'in kendisi (RFC 5789) yalnızca "bu değişiklik dokümanını uygula" der; dokümanın formatını media type belirler. İki standart aday var. JSON Merge Patch (RFC 7396, application/merge-patch+json) hedef dokümanın biçimine benzeyen bir object gönderir: gönderilen alan değişir, null gönderilen alan silinir, object'ler iç içe birleşir, array'ler ise bütün olarak değişir. JSON Patch (RFC 6902, application/json-patch+json) ise bir işlem listesidir: add, remove, replace, move, copy ve test; array içindeki tek bir elemanı index'le hedefleyebilir ve test ile "şu alan hâlâ şu değerdeyse uygula" diyebilir.
Merge Patch'in iki sınırı var: bir alana değer olarak null yazamazsın (çünkü null silmek demek) ve array'in tek elemanını değiştiremezsin. Bookstore'un writable alanları düz değerler ve küçük object'lerden oluştuğu için bu sınırlar bizi etkilemiyor; Merge Patch okunması ve Angular'dan üretilmesi en kolay format. Azure rehberi de PATCH için Merge Patch'i zorunlu tutar. Karar BS-42: PATCH yalnız Merge Patch kabul eder; başka bir media type gelirse RFC 5789'un önerdiği gibi 415 ve kabul edilen formatı söyleyen Accept-Patch header'ı döner. 2. sorunun cevabı bu:
$ curl -i -X PATCH -H "Authorization: Bearer ayse" -H "Content-Type: application/json" -d '{"price":{"amount":379.00}}' http://127.0.0.1:52318/books/9780140449266
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/problem+json
Date: Sat, 14 Nov 2026 22:37:01 GMT
Server: Kestrel
Transfer-Encoding: chunked
Accept-Patch: application/merge-patch+json
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.16","title":"Unsupported patch format","status":415,"detail":"PATCH /books/{isbn} accepts application/merge-patch+json only.","traceId":"00-1c554c2cc776d431261b9eb6f96546d3-cb391e893c01126e-00"}
$ curl -i -X PATCH -H "Authorization: Bearer ayse" -H "Content-Type: application/merge-patch+json" -d '{"price":{"amount":379.00},"description":"Unabridged, translated by Robin Buss."}' http://127.0.0.1:52318/books/9780140449266
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 14 Nov 2026 22:37:01 GMT
Server: Kestrel
ETag: "2"
Transfer-Encoding: chunked
{"isbn":"9780140449266","title":"The Count of Monte Cristo","author":"Alexandre Dumas","category":"classics","price":{"amount":379.00,"currency":"TRY"},"inStock":false,"description":"Unabridged, translated by Robin Buss."}
$ curl -s -X PATCH -H "Authorization: Bearer ayse" -H "Content-Type: application/merge-patch+json" -d '{"description":null}' http://127.0.0.1:52318/books/9780140449266
{"isbn":"9780140449266","title":"The Count of Monte Cristo","author":"Alexandre Dumas","category":"classics","price":{"amount":379.00,"currency":"TRY"},"inStock":false}
$ curl -s -X PATCH -H "Authorization: Bearer ayse" -H "Content-Type: application/merge-patch+json" -d '{"inStock":true}' http://127.0.0.1:52318/books/9780140449266 | jq -c '{status, title, detail}'
{"status":400,"title":"Field cannot be changed","detail":"'inStock' is read-only or unknown; writable fields: title, author, category, price, description."}
$ curl -s -X PATCH -H "Authorization: Bearer ayse" -H 'If-Match: "1"' -H "Content-Type: application/merge-patch+json" -d '{"price":{"amount":389.00}}' http://127.0.0.1:52318/books/9780140449266 | jq -c '{status, title, detail}'
{"status":412,"title":"Precondition failed","detail":"If-Match \"1\" does not match the current ETag \"3\"."}
Merge Patch'te price object'inin yalnız amount'u gönderildi, currency korundu. description: null alanı sildi; yanıtta artık yok. Salt okunur inStock'u değiştirme denemesi sessizce yok sayılmıyor, 400 alıyor; sessizce yok saymak istemciye "değişti" yanılgısı verir. Son istek eski bir ETag ile geldiği için 412 aldı: Ayşe'nin elindeki kitap 1. sürümdeydi, sunucudaki 3. sürümde. If-Match şimdilik isteğe bağlı; zorunlu hâle getirmek (428) ve optimistic concurrency'nin bütün contract'ı 07. yazıda.
Status Code Kararları: 400 mü, 422 mi, 409 mu?
4. sorunun cevabı en çok tartışılanı. Stokta 2 kitap varken 5 tane istemek bir yazım hatası değil; istek şemaya uygun, ama iş kuralı onu reddediyor. Bookstore'un ayrımı BS-44: 400 bozuk ya da şemaya aykırı istek, 422 geçerli ama bir iş kuralının reddettiği istek, 409 hedef resource'un mevcut durumuyla çakışma, 412 ise If-Match uyuşmazlığı. 422 eskiden WebDAV'a (RFC 4918) aitti, RFC 9110 onu "Unprocessable Content" adıyla genel HTTP'ye taşıdı.
$ curl -s -X PUT -H "Authorization: Bearer ada" -H "Content-Type: application/json" -d '{"quantity":5}' http://127.0.0.1:52318/customers/me/cart/items/9780441569595
{"type":"https://tools.ietf.org/html/rfc4918#section-11.2","title":"Insufficient stock","status":422,"detail":"Only 2 copies of Neuromancer are available.","availableQuantity":2,"traceId":"00-6c214af662521017f73bf1b7e6511595-d9eb85d677f44017-00"}
$ curl -s -X PUT -H "Authorization: Bearer ada" -H "Content-Type: application/json" -d '{"quantity":0}' http://127.0.0.1:52318/customers/me/cart/items/9780441569595 | jq -c '{status, title}'
{"status":400,"title":"One or more validation errors occurred."}
Adedin 0 olması şemayı ihlal ediyor (en az 1), o yüzden 400; satırı silmenin yolu DELETE. Stok yetersizliğini 409 ile dönen API'ler de var ve bu savunulabilir bir seçim. Önemli olan, ekibin bir kural seçip her endpoint'te aynı cevabı vermesi; istemci 422 gördüğünde kullanıcıya "adedi azalt" diyebilir, 400 gördüğünde formu düzeltmesini ister. Aşağıdaki tablo Bookstore'da kullandığımız kodları özetliyor:
| Kod | Ne zaman | Bookstore örneği |
|---|---|---|
| 200 | Okuma, değiştirme ya da custom method başarılı | GET kitap, PATCH fiyat, :cancel |
| 201 | Yeni resource oluştu; Location ile | POST kitap, PUT yeni ISBN, iade talebi |
| 202 | İş kabul edildi ama bitmedi | Fatura PDF üretimi (07. yazı) |
| 204 | Başarılı, dönülecek body yok | DELETE kitap, DELETE sepet satırı |
| 400 | Bozuk JSON, eksik ya da şemaya aykırı alan, salt okunur alan | Adet 0, PUT'ta eksik yazar |
| 401 | Kimlik bilgisi yok ya da geçersiz | Token'sız DELETE |
| 403 | Kimliği belli, yetkisi yok ve resource'un varlığını zaten biliyor | Müşterinin kitap silmesi, alıcının başka kampüsü listelemesi |
| 404 | Yok ya da çağıranın görmemesi gerekiyor | Başka kurumun siparişi, ikinci DELETE |
| 405 | Resource var, method desteklenmiyor; Allow ile | DELETE sipariş |
| 409 | Hedef resource'un durumuyla çakışma | Aynı ISBN, iptal edilmiş siparişi iptal |
| 412 | If-Match tutmuyor | Eski ETag ile PATCH |
| 415 | Body formatı desteklenmiyor | PATCH'te düz JSON |
| 422 | Geçerli istek, iş kuralı reddediyor | Stoktan fazla adet, TL dışı para birimi |
| 428 | Koşullu istek zorunlu | 07. yazıda |
| 429 | Rate limit aşıldı | 08. yazıda |
401, 403, 404 ve 405
Bu dörtlü en sık karıştırılanlar. 401 "kim olduğunu bilmiyorum" demek; 403 "kim olduğunu biliyorum, bu işe yetkin yok". Müşteri Ada'nın kitap silme denemesi 403, token'sız deneme 401 alıyor. Bir önceki yazıda gördüğümüz gibi, çağıranın varlığından haberdar olmaması gereken resource'larda 403 yerine 404 dönüyoruz (BS-45). 405 ise resource'un var olduğunu ama method'u desteklemediğini söyler ve RFC 9110'a göre Allow header'ı zorunludur:
$ curl -s -o /dev/null -w '%{http_code}\n' -X DELETE -H "Authorization: Bearer ada" http://127.0.0.1:52318/books/9780141439587
403
$ curl -s -o /dev/null -w '%{http_code}\n' -X DELETE http://127.0.0.1:52318/books/9780141439587
401
$ curl -i -X DELETE -H "Authorization: Bearer nevin" http://127.0.0.1:52318/orders/01a1c112-d7c0-706c-8cb9-c18a8fadc1a6
HTTP/1.1 405 Method Not Allowed
Content-Type: application/problem+json
Date: Sat, 14 Nov 2026 22:37:01 GMT
Server: Kestrel
Allow: GET, POST
Transfer-Encoding: chunked
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.6","title":"Method Not Allowed","status":405,"traceId":"00-c00de597c908d4b1bf399ac6cbc02319-0e1eedcb59a5e449-00"}
$ curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "Authorization: Bearer nevin" http://127.0.0.1:52318/orders/01a1c112-d7c0-706c-8cb9-c18a8fadc1a6
404
Son iki çıktıda küçük bir tutarsızlık var. Allow header'ı POST'u listeliyor ama aynı adrese POST 404 alıyor. POST aslında /orders/{orderId}:cancel custom method'una ait; ASP.NET Core routing'i, method kontrolünü complex segment'li bu endpoint'i de aday sayarak yapıyor gibi görünüyor. İstemciyi kırmayan ama contract'ta yazana uymayan bir ayrıntı; dokümante edilmesi ya da 405 yanıtının elle üretilmesi gerekir. Framework'ün davranışını contract'la karşılaştırmanın değeri tam da bu tür farkları görmek.
ASP.NET Core'da
MapGet yalnız GET'e cevap verir; aynı route'a HEAD gönderirsen 405 alırsın. Kitap endpoint'inde bu yüzden MapMethods("/{isbn}", ["GET", "HEAD"], GetBook) kullandık; Kestrel HEAD yanıtında body'yi göndermiyor, header'lar aynı kalıyor. 201 için TypedResults.Created(location, body), 204 için TypedResults.NoContent(), şema hataları için .NET 10'un AddValidation()'ı ve Data Annotations kullanılıyor. Merge Patch için ASP.NET Core'da hazır bir binder yok: body'yi JsonNode olarak okuyup RFC 7396'daki algoritmayı yirmi satırlık bir yardımcıyla uyguluyoruz; Accept-Patch ve If-Match kontrolü de elle yazılıyor. JSON Patch tercih edecek olanlar için .NET 10'daki System.Text.Json tabanlı JSON Patch desteği ASP.NET Core Web API serisinin ".NET 10 Yenilikleri" ve "JSON, Content Negotiation ve Formatter'lar" yazılarında anlatılıyor.
Sık Düşülen Tuzaklar
Her hatada 400 ya da 500 dönmek. İstemci 400'ü "isteğini düzelt", 500'ü "biraz sonra tekrar dene" diye okur. Stok yetersizliğine 500 dönen bir API, istemcinin retry policy'sini boşuna çalıştırır; çakışmaya 400 dönen API, kullanıcıya düzeltemeyeceği bir form hatası gösterir.
200 içinde "success": false. İlk yazıdaki /legacy/rpc bunun örneğiydi: monitoring, cache ve retry mekanizmaları hatayı göremez. Hata varsa status code onu söylemeli.
GET ile durum değiştirmek. GET /cart/add?isbn=… gibi bir link'i tarayıcının prefetch'i, bir link önizleme botu ya da crawler tetikleyebilir; reverse proxy de safe olduğunu varsayıp tekrar gönderebilir. BS-46: GET ve HEAD hiçbir şeyi değiştirmez.
PUT'u kısmi güncelleme için kullanmak. Gönderilmeyen alanı "değiştirme" diye yorumlayan bir PUT, istemcilerin hangi alanı bilerek boş bıraktığını ayırt edemez. Kısmi güncelleme PATCH'in işi.
Salt okunur alanları sessizce yok saymak. PATCH'te inStock göndermek 200 alıyorsa istemci stoğu değiştirdiğini sanar. Ya hata ver ya da contract'ta "yok sayılır" diye açıkça yaz.
Küçük Bir Deneme
★☆☆ Bir kitabı curl -I ile iste, gelen ETag'i If-Match header'ına koyup fiyatını PATCH et. Sonra aynı isteği aynı ETag ile tekrar gönder. İkinci yanıtın neden 412 olduğunu açıkla.
★★☆ Aynı fiyat değişikliğini JSON Patch (RFC 6902) dokümanı olarak yaz. test işlemiyle "fiyat hâlâ 379.00 ise 389.00 yap" koşulunu nasıl ifade edersin? Bunun If-Match'ten farkı ne?
★★★ Kurumsal siparişlerde en az 20 adet kuralı var. POST /institutions/{institutionId}/orders endpoint'ini contract'a tasarla: 15 adetlik istek, bilinmeyen ISBN, kurumun limiti aşılmış olması ve bozuk JSON için hangi status code'ları dönersin? Kararlarını BS-44'e göre gerekçelendir ve contract'ı lint'ten geçir.
Status code doğru seçildiğinde istemci neyin yanlış gittiğini kabaca bilir, ama neyin yanlış gittiğini tam olarak söyleyen şey hata body'si. Bu yazıdaki çıktılarda üç ayrı tutarsızlık gördük: RFC 4918'e giden 422 tipi, JSON'dan farklı yazılmış hata anahtarları ve her hatanın kendi cümlesi. Bir sonraki yazıda hata contract'ını RFC 9457 Problem Details ile baştan tasarlıyoruz.