Tipli hatalar, HTTP durum kodları, istek kimlikleri ve iç bilgileri sızdırmayan güvenli mesajları standartlaştıran Go API hata işleme kalıpları.

Her endpoint hata durumlarını farklı raporlarsa, istemciler API'ye güvenmeyi bırakır. Bir rota { "error": "not found" } döndürür, başka bir rota { "message": "missing" } döndürür, üçüncüsü ise düz metin gönderir. Anlam yakın olsa bile, istemci kodu ne olduğunu tahmin etmek zorunda kalır.
Maliyetler çabuk ortaya çıkar. Ekipler kırılgan ayrıştırma mantıkları kurar ve endpoint başına özel vakalar ekler. Tekrar denemeler riskli hale gelir çünkü istemci “daha sonra tekrar dene” ile “girdi hatalı”yı ayırt edemez. Destek talepleri artar çünkü istemci belirsiz bir mesaj görür ve ekibiniz bunu sunucu log satırıyla kolayca eşleştiremez.
Yaygın bir senaryo: bir mobil uygulama kayıt sırasında üç endpoint çağırır. İlki alan düzeyinde bir hata haritasıyla HTTP 400 döndürür, ikincisi bir stack trace içeren HTTP 500 döndürür, üçüncüsü { "ok": false } ile HTTP 200 döndürür. Uygulama ekibi üç farklı hata işleyicisi gönderir ve backend ekibi hâlâ “kayıt bazen başarısız oluyor” gibi nereden başlayacağını bilmediği raporlar alır.
Amaç tek, öngörülebilir bir sözleşme. İstemciler ne olduğunu, hatanın kendilerinden mi yoksa bizden mi kaynaklandığını, yeniden denemenin mantıklı olup olmadığını ve destek için yapıştırılabilecek bir istek kimliğini güvenle okuyabilmeliler.
Kapsam notu: Bu yazı JSON HTTP API'lerine odaklanır (gRPC değil), fakat aynı fikirler sistemlere hata döndürdüğünüz her yerde geçerlidir.
Hatalar için tek, net bir sözleşme seçin ve her endpointin buna uymasını sağlayın. “Tutarlı” olmak, aynı JSON yapısı, alanların aynı anlamı ve hangi handler başarısız olursa olsun aynı davranış demektir. Bunu yaptığınızda, istemciler tahmin etmeyi bırakır ve hataları düzgünce işler.
Kullanışlı bir sözleşme istemcinin bir sonraki adımı nasıl atacağını söylemelidir. Çoğu uygulama için her hata yanıtı üç soruyu yanıtlamalıdır:
Pratik kurallar:
Hangi öğelerin asla yanıtlarda görünmeyeceğine önceden karar verin. Yaygın “asla” öğeler: SQL parçaları, stack trace'ler, dahili host adları, gizli veriler ve bağımlılıklardan gelen ham hata dizeleri.
Temiz bir ayrımı koruyun: kısa, kullanıcıya yönelik bir mesaj (güvenli, nazik, yapılabilir) ve iç ayrıntılar (tam hata, stack ve bağlam) loglarda tutulmalı. Örneğin, “Değişiklikleriniz kaydedilemedi. Lütfen tekrar deneyin.” güvenlidir. “pq: duplicate key value violates unique constraint users_email_key” güvenli değildir.
Her endpoint aynı sözleşmeyi takip ettiğinde, istemciler tek bir hata işleyicisi oluşturup her yerde tekrar kullanabilir.
İstemciler yalnızca her endpoint aynı yapıda yanıt verirse hataları düzgün işleyebilir. Tek bir JSON zarfı seçin ve onu sabit tutun.
Pratik bir varsayılan, bir error nesnesi artı üst düzey bir request_id'dir:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Some fields are invalid.",
"details": {
"fields": {
"email": "must be a valid email address"
}
}
},
"request_id": "req_01HV..."
}
HTTP durum kodu geniş kategoriyi verir (400, 401, 409, 500). Makine tarafından okunabilir error.code istemcinin dallanabileceği spesifik durumu söyler. Bu ayrım önemlidir çünkü birçok farklı problem aynı durumu paylaşabilir. Bir mobil uygulama hem EMAIL_TAKEN hem de WEAK_PASSWORD için farklı UI gösterebilir, her ikisi de 400 olsa bile.
error.message kısa ve insan tarafından okunabilir olmalı. Kullanıcının problemi düzeltmesine yardımcı olmalı, fakat asla iç bilgileri sızdırmamalıdır (SQL, stack trace, sağlayıcı isimleri, dosya yolları).
İsteğe bağlı alanlar öngörülebilir kaldığı sürece faydalıdır:
details.fields.details.retry_after_seconds.details.docs_hint (URL değil).Geriye dönük uyumluluk için error.code değerlerini API sözleşmenizin bir parçası olarak ele alın. Yeni kodlar ekleyin ama eski anlamları değiştirmeyin. Sadece isteğe bağlı alanlar ekleyin ve istemcilerin tanımadıkları alanları yoksayacaklarını varsayın.
Her handler kendi başarısızlık sinyalleme yolunu uydurduğunda hata işleme karmaşıklaşır. Küçük bir tipli hata seti bunu düzeltir: handler'lar bilinen hata tiplerini döndürür, tek bir yanıt katmanı bunları tutarlı yanıtlara çevirir.
Pratik bir başlangıç seti çoğu endpoint'i kapsar:
Anahtar nokta üst seviyede istikrar sağlamak, kök neden değişse bile. Alt seviye hataları (SQL, ağ, JSON ayrıştırma) sarabilirsiniz ama middleware yine de tespit edebileceği aynı genel tipi alır.
type NotFoundError struct {
Resource string
ID string
Err error // private cause
}
func (e NotFoundError) Error() string { return "not found" }
func (e NotFoundError) Unwrap() error { return e.Err }
Handler'ınızda sql.ErrNoRows'u doğrudan sızdırmak yerine NotFoundError{Resource: "user", ID: id, Err: err} döndürün.
Hataları kontrol ederken errors.As kullanmayı tercih edin ve sentinel hatalar için errors.Is kullanın. Basit durumlar için sentinel hatalar (ör. var ErrUnauthorized = errors.New("unauthorized")) işe yarar, ama hangi kaynağın eksik olduğunu gibi güvenli bağlam gerektiğinde özel tipler daha iyidir çünkü genel kamu yanıt sözleşmenizi değiştirmeden bağlam eklemenizi sağlar.
Ekleyecekleriniz konusunda katı olun:
Err, stack bilgisi, ham SQL hataları, tokenlar, kullanıcı verileri.Bu ayrım, istemcilere yardımcı olurken iç bilgileri ifşa etmemenizi sağlar.
Tipli hatalara sahip olduktan sonra, bir sonraki iş sıkıcı ama gerekli: aynı hata tipi her zaman aynı HTTP durumu üretmeli. İstemciler buna göre mantık kuracak.
Çoğu API için işe yarayan pratik eşleme:
| Hata tipi (örnek) | Durum | Ne zaman kullanılır |
|---|---|---|
| BadRequest (bozuk JSON, eksik zorunlu query param) | 400 | İstek temel protokol veya format düzeyinde geçersiz. |
| Unauthenticated (token yok/geçersiz) | 401 | İstemcinin kimlik doğrulaması gerekiyor. |
| Forbidden (izin yok) | 403 | Kimlik doğrulama geçerli ama erişim yasak. |
| NotFound (kaynak ID yok) | 404 | İstenen kaynak yok (veya varlığını gizlemeyi seçtiniz). |
| Conflict (benzersiz kısıtlama, sürüm uyuşmazlığı) | 409 | İstek iyi biçimlendirilmiş ama mevcut durumla çakışıyor. |
| ValidationFailed (alan kuralları) | 422 | Yapı doğru ama iş doğrulaması başarısız. |
| RateLimited | 429 | Belirli bir zaman penceresinde çok fazla istek. |
| Internal (bilinmeyen hata) | 500 | Hata veya beklenmedik arıza. |
| Unavailable (bağımlılık down, zaman aşımı, bakım) | 503 | Geçici sunucu tarafı sorun. |
İki ayırım pek çok karışıklığı önler:
Tekrar deneme rehberliği önemlidir:
İstek kimliği (request ID), bir API çağrısını uçtan uca tanımlayan kısa benzersiz değerdir. İstemciler her yanıtta bunu görebiliyorsa, destek basitleşir: “İstek kimliğini gönderin” genellikle tam log satırını bulmak için yeterlidir.
Bu alışkanlık hem başarılı hem de hatalı yanıtlarda işe yarar.
Tek bir net kural kullanın: istemci bir istek kimliği gönderirse onu koruyun. Göndermediyse yeni bir kimlik oluşturun.
X-Request-Id).İstek kimliğini üç yerde koyun:
request_id olarak)Toplu endpoint'ler veya arka plan işleri için üst (parent) bir request ID tutun. Örnek: bir istemci 200 satır yüklüyor, 12 tane doğrulama hatası alıyor ve işler kuyruğa alınıyor. Tüm çağrı için tek bir request_id döndürün ve her iş veya öğe düzeyindeki hataya parent_request_id ekleyin. Böylece bir yüklemenin birden çok göreve yayılması durumunda bile izleme mümkün olur.
İstemciler net, sabit bir hata yanıtına ihtiyaç duyar. Loglarınızsa dağınık gerçeği tutmalı. Bu iki dünyayı ayırın: istemciye güvenli bir mesaj ve kamu hata kodu verin, sunucuda ise iç nedeni, stack ve bağlamı loglayın.
Her hata yanıtı için request_id ile aranabilir bir yapılandırılmış olay loglayın.
Tutarlı tutulması faydalı alanlar:
İç ayrıntıları sadece sunucu loglarında (veya iç hata deposunda) saklayın. İstemci hiçbir zaman ham veritabanı hatası, sorgu metni, stack trace veya sağlayıcı mesajı görmemeli. Çoklu servis çalıştırıyorsanız source (api, db, auth, upstream) gibi dahili bir alan triage'ı hızlandırır.
Gürültülü endpoint'leri ve sık tekrarlanan hata türlerini izleyin. Bir endpoint aynı 429 veya 400'ü dakikada binlerce kez üretebiliyorsa log spam'inden kaçının: tekrarlayan olayları örnekleyin veya beklenen hatalar için seviyeyi düşürün, ama yine de metriklerde sayımlarını tutun.
Metrikler loglardan daha erken sorun yakalar. HTTP durumuna ve hata koduna göre gruplanmış sayıları takip edin ve ani sıçramalara alarm koyun. RATE_LIMITED deploy sonrası 10 kat artarsa, log örneklenmiş olsa bile metrikler sayesinde hızlıca fark edersiniz.
Hataları tutarlı hale getirmenin en kolay yolu onları her yerde “yönetmekten” vazgeçip küçük bir hattın içinden geçirmek. Bu hat, istemcinin gördüğünü ve sizin loglarda ne tuttuğunuzu karar verir.
Küçük bir hata kodu setiyle başlayın (örnek: INVALID_ARGUMENT, NOT_FOUND, UNAUTHORIZED, CONFLICT, INTERNAL). Bunları güvenli, kamuya açık alanları (code, safe message, hangi alanın yanlış olduğu gibi isteğe bağlı detaylar) ortaya koyan tipli hatalara sarın. İç nedenleri saklı tutun.
Sonra herhangi bir hatayı (statusCode, responseBody)'ye çeviren tek bir çevirici fonksiyon uygulayın. Burada tipli hatalar HTTP durumlarına eşlenir ve bilinmeyen hatalar güvenli bir 500 yanıtına dönüşür.
Ardından middleware ekleyin:
request_id'si olduğundan emin olsunBir panic asla stack trace'i istemciye dökmemeli. Normal bir 500 yanıtı ve genel bir mesaj döndürün, tam panic bilgisini aynı request_id ile loglayın.
Son olarak, handler'larınızı doğrudan yanıt yazmak yerine error dönecek şekilde değiştirin. Bir wrapper handler'ı çağırır, çeviriciyi çalıştırır ve standart formatta JSON yazar.
Kısa kontrol listesi:
Golden test'ler sözleşmeyi kilitlediği için önemlidir. Biri daha sonra bir mesajı veya durum kodunu değiştirirse, testler müşteriler etkilenmeden önce başarısız olur.
Bir endpoint hayal edin: bir istemci müşteri kaydı oluşturuyor.
POST /v1/customers gövdesi { "email": "[email protected]", "name": "Pat" } gibi. Sunucu her zaman aynı hata şeklini döndürür ve her zaman bir request_id içerir.
E-posta eksik veya yanlış formatta. İstemci alanı vurgulayabilir.
{
"request_id": "req_01HV9N2K6Q7A3W1J9K8B",
"error": {
"code": "VALIDATION_FAILED",
"message": "Some fields need attention.",
"details": {
"fields": {
"email": "must be a valid email address"
}
}
}
}
E-posta zaten mevcut. İstemci oturum açmayı veya başka bir e-posta seçmeyi önerebilir.
{
"request_id": "req_01HV9N3C2D0F0M3Q7Z9R",
"error": {
"code": "ALREADY_EXISTS",
"message": "A customer with this email already exists."
}
}
Bir bağımlılık kapalı. İstemci geri planlı yeniden deneme gösterip sakin bir mesaj gösterebilir.
{
"request_id": "req_01HV9N3X8P2J7T4N6C1D",
"error": {
"code": "TEMPORARILY_UNAVAILABLE",
"message": "We could not save your request right now. Please try again."
}
}
Tek bir sözleşme ile istemci şöyle tepki verir:
details.fields kullanarak alanları işaretlerequest_id'yi destek için gösterDestek için aynı request_id iç loglarda gerçek nedeni bulmanın en hızlı yoludur; stack trace veya veritabanı hatalarını ifşa etmeden.
İstemcileri en hızlı sinirlendiren şey onları tahmin etmeye zorlamaktır. Bir endpoint { "error": "..." } döndürürken diğeri { "message": "..." } döndürürse, her istemci özel durumlardan oluşan bir yığın haline gelir ve hatalar haftalarca gizlenir.
Tekrarlayan birkaç hata:
code kullanmamak.request_id'yi sadece hatalarda eklemek; böylece bir kullanıcı raporu ile başarılı çağrıyı ilişkilendiremezsiniz.İç bilgileri sızdırmak en kolay düşülen tuzaktır. Handler err.Error() dönmek kolaydır ama bu constraint adı veya üçüncü taraf mesajının üretim ortamında yanıt olarak gitmesine yol açar. Kullanıcı mesajını kısa ve güvenli tutun; detaylı nedeni loglarda bırakın.
Sadece metne güvenmek de yavaşça sorun çıkarır. İstemci İngilizce bir cümleyi ayrıştırmak zorunda kalıyorsa ("email already exists"), metni değiştirince istemci mantığı bozulur. Sabit hata kodları mesajları değiştirip çevirseniz bile davranışı korur.
Hata kodlarını kamu sözleşmenizin bir parçası olarak ele alın. Değiştirmeniz gerekirse yeni bir kod ekleyin ve eski kod bir süre çalışmaya devam etsin. Son olarak, her yanıtta, başarı veya hata fark etmeksizin aynı request_id alanını koyun. Bir kullanıcı "önce çalıştı, sonra bozuldu" dediğinde o tek ID genellikle bir saati kurtarır.
Yayın öncesi tutarlılık için hızlı bir kontrol:
error.code, error.message, request_id).VALIDATION_FAILED, NOT_FOUND, CONFLICT, UNAUTHORIZED). Handler'ların bilinmeyen kod döndürmemesi için test ekleyin.request_id döndürün ve panikler ve zaman aşımı dahil her istekte loglayın.Bunlardan sonra birkaç endpoint'i elle kontrol edin. Bir doğrulama hatası, eksik kayıt ve beklenmedik hata tetikleyin. Yanıtlar endpoint'ler arasında farklı görünüyorsa (alanlar değişiyor, durum kodları sürükleniyor, mesajlar iç bilgi sızdırıyor) paylaşılmış hattı düzeltmeden yeni özellik eklemeyin.
Pratik bir kural: bir mesaj bir saldırgana yardım ederse ya da normal kullanıcıyı kafa karıştırıyorsa, o mesaj loglarda olmalı, yanıtta değil.
İstemci tarafında her endpointin takip etmesini istediğiniz hata sözleşmesini yazılı hale getirin, API zaten yayındaysa bile. Paylaşılan bir sözleşme (durum, sabit hata kodu, güvenli mesaj ve request_id) istemciler için hataları öngörülebilir kılmanın en hızlı yoludur.
Sonra kademeli olarak taşıyın. Mevcut handler'ları tutun ama hatalarını tek bir eşleyici aracılığıyla kamu yanıt biçiminize çevirin. Bu, büyük bir yeniden yazma yapmadan tutarlılığı artırır ve yeni endpoint'lerin yeni formatlar icat etmesini engeller.
Küçük bir hata kodu kataloğu tutun ve bunu API'nizin bir parçası gibi ele alın. Yeni bir kod eklemek isteyen biri olduğunda hızlıca gözden geçirin: gerçekten yeni mi, adı açık mı ve doğru HTTP durumuna mı eşleniyor?
Sözleşme sürüntüsünü yakalamak için bir kaç test ekleyin:
request_id içerir.error.code mevcut ve katalogdan geliyor.error.message güvenli ve iç detaylar içermiyor.Sıfırdan bir Go backend inşa ediyorsanız, sözleşmeyi erken kilitlemek yardımcı olur. Örneğin, Koder.ai (koder.ai) planlama modunda hata şeması ve kod kataloğu gibi konvansiyonları önceden tanımlamanıza olanak vererek API büyürken handler'ların uyumlu kalmasını sağlar.
Her endpoint için tek bir JSON biçimi kullanın. Pratik bir varsayılan, üst düzey bir request_id ile birlikte code, message ve isteğe bağlı details alanlarına sahip bir error nesnesidir; böylece istemciler güvenilir şekilde ayrıştırıp tepki verebilirler.
error.message alanını kısa ve kullanıcı için güvenli bir cümle olarak döndürün, gerçek nedeni sunucu loglarında tutun. Ham veritabanı hataları, stack trace'ler, iç host adları veya üçüncü taraf mesajlarını asla döndürmeyin, geliştirme sırasında bile.
Makine mantığı için sabit bir error.code kullanın; HTTP durum kodları geniş kategoriyi belirtir. İstemciler ALREADY_EXISTS gibi error.code değerine göre dallanmalı ve durumu 409 gibi HTTP kodu sadece genel rehber olarak görmelidir.
İstek güvenilir şekilde ayrıştırılamıyorsa (bozuk JSON, yanlış tipler) 400 kullanın. İstek iyi biçimlendirilmiş ama iş kuralları başarısız oluyorsa (geçersiz e-posta formatı, kısa parola) 422 kullanın.
Girdi geçerli ama mevcut durumla çakışıyorsa (e-posta zaten kayıtlı, versiyon uyuşmazlığı) 409 kullanın. Alan düzeyinde doğrulama ve değeri değiştirerek çözülebilecek durumlar için 422 kullanın.
Küçük bir tipli hata seti oluşturun (validation, not found, conflict, unauthorized, internal) ve handler'ların bunları döndürmesini sağlayın. Ardından bu tipleri tek bir paylaşılan çevirici ile HTTP durum kodlarına ve standart JSON yanıt biçimine eşleyin.
Başarılı veya başarısız her yanıtta request_id döndürün ve sunucudaki her log satırına bunu ekleyin. Bir kullanıcı bir sorun bildirirse, o tek ID genellikle tam başarısızlık yolunu loglarda bulmak için yeterlidir.
Operasyon başarılı olduğunda 200 döndürün; hataları 4xx/5xx ile gösterin. Hataları 200 içinde saklamak, istemcilerin gövde alanlarını ayrıştırmasını zorunlu kılar ve endpoint'ler arasında tutarsız davranışa yol açar.
Genelde 400, 401, 403, 404, 409 ve 422 için yeniden denemeyi önermeyin; bunlar değişiklik olmadan düzelmez. 503 için tekrar deneyin; 429 bazen bekledikten sonra yeniden denenebilir. Idempotency anahtarları destekleniyorsa, POST gibi işlemler geçici hatalarda daha güvenli şekilde yeniden denenebilir.
Sözleşmeyi “golden” testlerle kilitleyin: her hata yanıtı request_id içeriyor mu, durum kodu hatanın tipine uygun mu, error.code katalogdaki bir kod mu gibi kontroller ekleyin. Yeni kod eklerken eski davranışları kırmamaya dikkat edin.