API Hata Yönetimi: Anlamlı HTTP Status Kodları Nasıl Kullanılır?

Bir API tasarlarken en çok göz ardı edilen konulardan biri HTTP durum kodlarının doğru kullanımıdır. “Çalıştı mı? 200 döndür. Çalışmadı mı? 500 döndür.” mantığıyla yazılmış API’lerle karşılaşmak, bir sysadmin olarak insanın içini karartıyor. Üstelik bu sadece estetik bir sorun değil; yanlış durum kodları monitoring sistemlerini yanıltır, istemci tarafında hatalı retry mekanizmalarına yol açar ve debug sürecini cehennem haline getirir. Bu yazıda HTTP durum kodlarını nasıl anlamlı şekilde kullanacağımızı, gerçek dünya senaryolarıyla ele alacağız.

HTTP Durum Kodları Neden Bu Kadar Önemli?

Bir REST API’nin sadece geliştiricilerle değil, aynı zamanda load balancer’lar, CDN’ler, monitoring araçları ve otomatik retry sistemleriyle de konuştuğunu unutmamak gerekir. Nginx bir 502 gördüğünde upstream sunucunun öldüğünü anlar. Bir Kubernetes readiness probe 503 alırsa pod’u traffic’ten çıkarır. CloudFlare 429 görünce rate limiting devreye girdiğini anlar.

Yani durum kodları sadece “hata var mı yok mu” sorusuna cevap vermez; hatanın kimin sorumluluğunda olduğunu, tekrar denenip denenmeyeceğini ve ne yapılması gerektiğini de söyler.

Bunu bir benzetmeyle açıklamak gerekirse: Bir kargo şirketi düşünün. “Teslimat başarısız” demek yeterli değil. “Adres yanlış mıydı, kapıda kimse yoktu mı, depo mu doluydu, araç mı arızalandı?” – bunların hepsi farklı aksiyonlar gerektirir. HTTP durum kodları da tam olarak bu işi yapar.

2xx: Başarı Kodlarının İncelikleri

Başarı durumlarında da çoğu geliştirici her şeye 200 yapıştırır. Oysa 2xx ailesinin her üyesinin farklı bir anlamı var.

200 OK: En genel başarı kodu. GET isteklerinde, body dolu döndüğünde kullanılır.

201 Created: Bir kaynak oluşturulduğunda kullanılır. POST ile yeni bir kullanıcı, sipariş veya kayıt oluşturduğunuzda 200 değil 201 dönmelisiniz. Üstelik Location header’ında yeni oluşturulan kaynağın URL’ini de vermelisiniz.

202 Accepted: Asenkron işlemler için altın değerinde bir kod. İstek alındı, işleniyor ama henüz tamamlanmadı demektir. Büyük dosya yükleme işlemleri, e-posta gönderme kuyrukları veya uzun süren raporlar için idealdir.

204 No Content: Başarılı ama döndürecek bir şey yok. DELETE işlemlerinde veya PUT ile güncelleme yapıp sonucu döndürmek istemediğinizde kullanılır.

# Kullanıcı oluşturma - 201 örneği
curl -X POST https://api.example.com/users 
  -H "Content-Type: application/json" 
  -d '{"username": "ahmet", "email": "[email protected]"}' 
  -v

# Beklenen yanıt:
# HTTP/1.1 201 Created
# Location: /users/12345
# Content-Type: application/json
#
# {"id": 12345, "username": "ahmet", "email": "[email protected]"}
# Asenkron rapor oluşturma - 202 örneği
curl -X POST https://api.example.com/reports/generate 
  -H "Content-Type: application/json" 
  -d '{"type": "monthly", "year": 2024, "month": 11}' 
  -v

# Beklenen yanıt:
# HTTP/1.1 202 Accepted
# Content-Type: application/json
#
# {"job_id": "rpt_789xyz", "status": "queued", "check_url": "/reports/status/rpt_789xyz"}

4xx: İstemci Hatalarını Doğru Kategorize Etmek

4xx kodları “bu hata senin hatan” demektir. İstemciye “bir şeyler yanlış yaptın, şunu düzelt” mesajı veriyoruz. Bu mesajı ne kadar netleştirirsek, entegrasyon yapan ekipler o kadar hızlı sorunu çözer.

400 Bad Request

En çok kötüye kullanılan kodlardan biri. “Bir şeyler yanlış ama ne olduğunu söylemeyeceğim” yaklaşımı. Oysa 400 döndürürken body’de tam olarak neyin yanlış olduğunu belirtmeliyiz.

# Kötü 400 yanıtı örneği - YAPMAYIN
curl -X POST https://api.example.com/orders 
  -H "Content-Type: application/json" 
  -d '{"product_id": "abc", "quantity": -5}'

# Kötü yanıt:
# HTTP/1.1 400 Bad Request
# {"error": "Bad Request"}

# İyi 400 yanıtı - BUNU YAPIN
# HTTP/1.1 400 Bad Request
# {
#   "error": "validation_failed",
#   "message": "Istek dogrulanamadi",
#   "details": [
#     {"field": "product_id", "message": "product_id sayisal olmalidir"},
#     {"field": "quantity", "message": "Miktar 1 veya daha buyuk olmalidir"}
#   ]
# }

401 vs 403: Bu İkisini Karıştırmayın

Bu iki kod arasındaki fark kritiktir ve sürekli karıştırılır.

401 Unauthorized: Aslında “kimliğiniz doğrulanmadı” demektir. Token yoktur, süresi dolmuştur veya geçersizdir. İstemci yeniden login olup yeni token almalıdır. WWW-Authenticate header’ı da eklenmelidir.

403 Forbidden: Kimliğiniz doğrulandı ama bu kaynağa erişim yetkiniz yok. Yönetici paneline giren normal kullanıcı, başkasının dosyasını silmeye çalışan kullanıcı. Bu durumda tekrar login olmak çözüm değildir.

# 401 - Token süresi dolmuş
curl -X GET https://api.example.com/profile 
  -H "Authorization: Bearer eyJhbGc...expired_token"

# Yanıt:
# HTTP/1.1 401 Unauthorized
# WWW-Authenticate: Bearer realm="api.example.com", error="token_expired"
# {
#   "error": "token_expired",
#   "message": "Erisim tokeninizin suresi dolmus, yeniden giris yapin",
#   "refresh_endpoint": "/auth/refresh"
# }

# 403 - Yetki yok
curl -X DELETE https://api.example.com/users/999/data 
  -H "Authorization: Bearer eyJhbGc...valid_token_but_no_admin"

# Yanıt:
# HTTP/1.1 403 Forbidden
# {
#   "error": "insufficient_permissions",
#   "message": "Bu islemi gerceklestirmek icin admin yetkisi gereklidir",
#   "required_role": "admin"
# }

404 vs 410: Kaynak Gerçekten Gitti mi?

404 Not Found: Kaynak bulunamadı, ama belki hiç olmadı da olabilir.

410 Gone: Kaynak eskiden vardı ama artık yok ve geri gelmeyecek. Silinen kullanıcı profilleri, kaldırılan API endpoint’leri için bunu kullanın. SEO açısından da önemlidir; arama motorları 410 gördüklerinde indexten daha hızlı kaldırır.

422 Unprocessable Entity

Bu kod özellikle REST API’lerde çok değerlidir. 400’den farkı şudur: İstek formatı doğru (valid JSON, doğru Content-Type) ama iş mantığı açısından işlenemiyor. Örneğin banka havalesi yaparken bakiyeyi aşan miktar girilmesi.

# 422 - İş mantığı hatası
curl -X POST https://api.example.com/transfers 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer valid_token" 
  -d '{
    "from_account": "TR123456",
    "to_account": "TR789012", 
    "amount": 50000,
    "currency": "TRY"
  }'

# Yanıt:
# HTTP/1.1 422 Unprocessable Entity
# {
#   "error": "business_rule_violation",
#   "message": "Transfer gerceklestirilemedi",
#   "reason": "insufficient_balance",
#   "available_balance": 12500.00,
#   "requested_amount": 50000.00,
#   "currency": "TRY"
# }

429 Too Many Requests: Rate Limiting’in Doğru Uygulanması

Rate limiting sadece 429 döndürmekten ibaret değil. Header’lar aracılığıyla istemciye ne zaman tekrar deneyebileceğini söylemelisiniz.

# Rate limit bilgileri header'larda olmalı
curl -X GET https://api.example.com/search?q=linux 
  -H "Authorization: Bearer valid_token"

# Normal yanıt header'ları:
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 23
# X-RateLimit-Reset: 1732900800

# Limit aşıldığında:
# HTTP/1.1 429 Too Many Requests
# Retry-After: 47
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 0
# X-RateLimit-Reset: 1732900800
# {
#   "error": "rate_limit_exceeded",
#   "message": "API limitinizi astiniz",
#   "retry_after_seconds": 47,
#   "limit": 100,
#   "window": "1 saat"
# }

5xx: Sunucu Hatalarını Doğru Yönetmek

5xx kodları “bu hata bizim hatamız” demektir. İstemciye “sen doğru bir şey yaptın, sorun bizde” mesajı verir. Bu ayrım hem UX hem de monitoring açısından kritiktir.

500 Internal Server Error

Beklenmeyen hatalar için kullanılır. Ama dikkat: 500 döndürürken stack trace’i asla istemciye göndermeyin. Bu bir güvenlik açığıdır. Bunun yerine bir correlation ID verin; log’larınızda o ID ile detaylı hata bilgisini bulabilirsiniz.

# Kötü 500 yanıtı - GÜVENLİK AÇIĞI
# HTTP/1.1 500 Internal Server Error
# {
#   "error": "NullPointerException at UserService.java:147",
#   "stack_trace": "com.example.UserService.getUserById(UserService.java:147)...",
#   "database_query": "SELECT * FROM users WHERE id=..."
# }

# Doğru 500 yanıtı
# HTTP/1.1 500 Internal Server Error
# {
#   "error": "internal_error",
#   "message": "Beklenmeyen bir hata olustu",
#   "correlation_id": "err_2024_xK9mN3pQ",
#   "support_contact": "[email protected]"
# }

502, 503, 504: Bunları Ayırt Etmek

Bu üç kod özellikle mikroservis mimarilerinde çok önemlidir.

502 Bad Gateway: Upstream sunucudan geçersiz yanıt alındı. Backend servis çökmüş ama proxy ayakta.

503 Service Unavailable: Servis geçici olarak kullanılamıyor. Bakım modunda, aşırı yüklenme veya circuit breaker açık. Retry-After header’ı eklenmelidir.

504 Gateway Timeout: Upstream sunucu zamanında yanıt vermedi. Timeout problemi.

# Nginx upstream konfigürasyonu ile doğru hata yönetimi
cat /etc/nginx/conf.d/api-proxy.conf

# upstream api_backend {
#     server app1.internal:8080;
#     server app2.internal:8080;
# }
#
# server {
#     location /api/ {
#         proxy_pass http://api_backend;
#         proxy_connect_timeout 5s;
#         proxy_read_timeout 30s;
#         
#         # 502/504 durumunda backup sunucuya git
#         proxy_next_upstream error timeout http_502 http_504;
#         
#         # Custom hata sayfaları
#         error_page 502 /errors/502.json;
#         error_page 503 /errors/503.json;
#         error_page 504 /errors/504.json;
#     }
# }

Gerçek Dünya Senaryosu: E-Ticaret API’si

Bir e-ticaret platformunda sipariş oluşturma akışını ele alalım. Her adımda farklı hatalar oluşabilir ve bunların doğru kodlarla iletilmesi gerekir.

#!/bin/bash
# Siparis olusturma API'si test scripti
# Farkli hata senaryolarini simule eder

API_BASE="https://api.shop.example.com/v1"
TOKEN="Bearer test_token_12345"

echo "=== Senaryo 1: Gecersiz token ==="
curl -s -o /dev/null -w "HTTP Status: %{http_code}n" 
  -X POST "${API_BASE}/orders" 
  -H "Authorization: Bearer invalid_token" 
  -H "Content-Type: application/json" 
  -d '{"product_id": 1, "quantity": 2}'
# Beklenen: HTTP Status: 401

echo ""
echo "=== Senaryo 2: Eksik zorunlu alan ==="
curl -s -o /dev/null -w "HTTP Status: %{http_code}n" 
  -X POST "${API_BASE}/orders" 
  -H "Authorization: ${TOKEN}" 
  -H "Content-Type: application/json" 
  -d '{"product_id": 1}'
# Beklenen: HTTP Status: 400

echo ""
echo "=== Senaryo 3: Stokta olmayan urun ==="
curl -s -o /dev/null -w "HTTP Status: %{http_code}n" 
  -X POST "${API_BASE}/orders" 
  -H "Authorization: ${TOKEN}" 
  -H "Content-Type: application/json" 
  -d '{"product_id": 9999, "quantity": 100}'
# Beklenen: HTTP Status: 422

echo ""
echo "=== Senaryo 4: Basarili siparis ==="
curl -s -w "nHTTP Status: %{http_code}n" 
  -X POST "${API_BASE}/orders" 
  -H "Authorization: ${TOKEN}" 
  -H "Content-Type: application/json" 
  -d '{"product_id": 42, "quantity": 1, "address_id": 7}'
# Beklenen: HTTP Status: 201

Hata Yanıtlarında Tutarlılık: Error Schema Standartı

Her endpoint farklı formatta hata döndürüyorsa, entegrasyon yapan ekiplerin işi çok zorlaşır. RFC 7807 (Problem Details for HTTP APIs) standardını benimsemek çok iyi bir pratiktir.

# RFC 7807 uyumlu hata yaniti ornegi
# Content-Type: application/problem+json

# {
#   "type": "https://api.example.com/errors/insufficient-stock",
#   "title": "Yetersiz Stok",
#   "status": 422,
#   "detail": "Urun 42 icin 10 adet talep edildi, ancak stokta yalnizca 3 adet bulunuyor",
#   "instance": "/orders/attempt/2024-11-29-abc123",
#   "product_id": 42,
#   "requested_quantity": 10,
#   "available_quantity": 3
# }

# Bu formati test eden curl komutu:
curl -v -X POST https://api.example.com/orders 
  -H "Content-Type: application/json" 
  -H "Accept: application/json, application/problem+json" 
  -d '{"product_id": 42, "quantity": 10}' 2>&1 | 
  grep -E "HTTP/|content-type:|{" | head -20

Monitoring ve Alerting: Durum Kodlarını İzlemek

Doğru durum kodları sayesinde monitoring çok daha anlamlı hale gelir. Prometheus ile basit bir örnek:

# Prometheus + Grafana için nginx log formatini duzenle
# /etc/nginx/nginx.conf icinde log_format tanimla

log_format api_json escape=json
  '{"time":"$time_iso8601",'
  '"method":"$request_method",'
  '"uri":"$uri",'
  '"status":$status,'
  '"response_time":$request_time,'
  '"upstream_time":"$upstream_response_time",'
  '"client_ip":"$remote_addr"}';

# Prometheus alerting kurallari (alert.rules.yml)
# groups:
#   - name: api_errors
#     rules:
#       - alert: HighClientErrorRate
#         expr: rate(http_requests_total{status=~"4.."}[5m]) > 0.1
#         annotations:
#           summary: "Yuksek 4xx hata orani tespit edildi"
#
#       - alert: ServerErrorSpike  
#         expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.01
#         for: 2m
#         annotations:
#           summary: "5xx hatalari artisi - acil mudahale gerekebilir"

# Log'lardan hata dagilimini analiz et
awk '{print $9}' /var/log/nginx/api_access.log | 
  sort | uniq -c | sort -rn | head -10

Retry Mekanizmaları ve Idempotency

Hangi durum kodlarının retry edilmesi gerektiğini bilmek kritiktir. Yanlış bir retry, iki kez para çekmeye veya iki sipariş oluşmaya neden olabilir.

Güvenli retry edilebilir kodlar:

  • 408 Request Timeout: Evet, retry edilebilir
  • 429 Too Many Requests: Evet, Retry-After süresi beklenip retry edilebilir
  • 500 Internal Server Error: Dikkatli retry, idempotent endpoint’lerde uygulanabilir
  • 502 Bad Gateway: Evet, retry edilebilir
  • 503 Service Unavailable: Evet, Retry-After ile retry edilebilir
  • 504 Gateway Timeout: Evet, idempotent işlemlerde retry edilebilir

Retry edilmemesi gereken kodlar:

  • 400 Bad Request: İstek düzeltilmeden retry işe yaramaz
  • 401 Unauthorized: Önce authentication gerekli
  • 403 Forbidden: Retry sonucu değiştirmez
  • 404 Not Found: Kaynak yoksa retry anlamsız
  • 422 Unprocessable Entity: İş mantığı hatası, retry çözüm değil
# Akilli retry scripti
#!/bin/bash

make_api_call() {
  local url=$1
  local data=$2
  local max_retries=3
  local retry_count=0
  
  while [ $retry_count -lt $max_retries ]; do
    response=$(curl -s -w "n%{http_code}" -X POST "$url" 
      -H "Content-Type: application/json" 
      -H "Idempotency-Key: $(uuidgen)" 
      -d "$data")
    
    http_code=$(echo "$response" | tail -1)
    body=$(echo "$response" | head -n -1)
    
    case $http_code in
      200|201|202|204)
        echo "Basarili: HTTP $http_code"
        echo "$body"
        return 0
        ;;
      429)
        retry_after=$(curl -s -I -X POST "$url" -d "$data" | 
          grep -i "retry-after:" | awk '{print $2}' | tr -d 'r')
        echo "Rate limit. ${retry_after:-60} saniye bekleniyor..."
        sleep "${retry_after:-60}"
        ;;
      503)
        echo "Servis gecici olarak kullanilamıyor. 10 saniye bekleniyor..."
        sleep 10
        ;;
      400|401|403|404|422)
        echo "Kurtarilabilir olmayan hata: HTTP $http_code"
        echo "$body"
        return 1
        ;;
      *)
        echo "Beklenmeyen HTTP $http_code, retry ${retry_count}/${max_retries}"
        sleep $((2 ** retry_count))
        ;;
    esac
    
    retry_count=$((retry_count + 1))
  done
  
  echo "Maksimum retry sayisina ulasildi"
  return 1
}

make_api_call "https://api.example.com/orders" '{"product_id": 42, "quantity": 1}'

Güvenlik Açısından Dikkat Edilmesi Gerekenler

Durum kodları yanlış kullanıldığında güvenlik açıklarına da yol açabilir.

Enumeration saldırılarına karşı dikkat: Kullanıcı bulunamadı durumunda 404, kullanıcı var ama şifre yanlışsa 401 döndürmek, saldırgana hangi kullanıcı adlarının sistemde kayıtlı olduğunu söyler. Bunun yerine her iki durumda da 401 döndürmek daha güvenlidir.

Bilgi sızıntısı: 500 hatalarında database adları, tablo yapıları veya internal IP’ler gibi bilgiler kesinlikle istemciye iletilmemelidir. Correlation ID kullanımı burada hem güvenlik hem de debug kolaylığı sağlar.

Timing saldırıları: Bazı durumlarda farklı durum kodları döndürme süreleri bile bilgi sızıntısına neden olabilir. Sabit response time hedeflemek iyi bir pratiktir.

Sonuç

HTTP durum kodlarını doğru kullanmak, iyi bir API tasarımının temel taşlarından biridir. Bu yazıda ele aldığımız noktaları özetleyecek olursak:

2xx kodlarında sadece 200’e sığınmak yerine 201, 202 ve 204’ü uygun yerlerde kullanmak, API’nizin davranışını çok daha açık hale getirir. 4xx tarafında 401 ile 403’ü doğru ayırt etmek, istemcilerin ne yapmaları gerektiğini net anlamasını sağlar. 422 kodu, iş mantığı hatalarını format hatalarından ayırmak için kritiktir. 5xx hatalarında ise correlation ID kullanımı hem güvenliği artırır hem de production debug sürecini dramatik ölçüde kolaylaştırır.

Rate limiting için 429 kullanırken Retry-After header’ını eklemeyi, asenkron işlemler için 202 döndürürken job status URL’ini vermeyi, hata formatlarında RFC 7807’yi referans almayı ve monitoring sistemlerinizi 4xx/5xx oranları üzerinden uyarı verecek şekilde yapılandırmayı ihmal etmeyin.

Son olarak şunu söylemek gerekir: Mükemmel durum kodu seçimi bazen tartışmalı olabilir. Önemli olan tutarlı olmak ve ekibinizle bir karar mekanizması oluşturmaktır. Bir API’de 400 ile 422 arasındaki sınırı net çizmek ve tüm endpoint’lerde aynı mantığı uygulamak, sürekli değişen kurallardan çok daha değerlidir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir