API Yanıtlarını İşleme: JSON Parse ve Hata Yönetimi

Modern sistemlerde neredeyse her şey bir API üzerinden konuşuyor. Monitoring sistemlerin veri çektiği servisler, deployment pipeline’larının tetiklediği webhook’lar, kullanıcı kimlik doğrulama akışları… Hepsinin ortak noktası JSON formatında veri alışverişi yapması. Peki bu verileri bash script’lerinde veya otomasyon araçlarında düzgün işleyemizsek ne olur? En iyi ihtimalle yanlış bir değer okursun, en kötü ihtimalle production sisteminde sessiz sedasız bir hata oluşur ve saatler sonra fark edersin. Bu yazıda API yanıtlarını nasıl doğru parse edeceğimizi, hataları nasıl yöneteceğimizi ve gerçek dünya senaryolarında nelere dikkat etmemiz gerektiğini ele alacağız.

Temel Araçlar: jq ile Tanışma

Linux dünyasında JSON işlemenin standart aracı jq‘dur. Hafif, hızlı ve son derece güçlü bir komut satırı aracı. Eğer henüz kurulu değilse:

# Debian/Ubuntu
apt-get install jq -y

# RHEL/CentOS
yum install jq -y

# macOS
brew install jq

jq‘nun temel mantığını anlamak için önce basit bir örnekle başlayalım. Bir API’den kullanıcı bilgisi çektiğimizi varsayalım:

#!/bin/bash
# Basit JSON parse örneği

API_RESPONSE='{
  "status": "success",
  "user": {
    "id": 1234,
    "name": "Ahmet Yilmaz",
    "email": "[email protected]",
    "roles": ["admin", "developer"]
  }
}'

# Tek bir alan okuma
USERNAME=$(echo "$API_RESPONSE" | jq -r '.user.name')
echo "Kullanici: $USERNAME"

# Array elemanına erişim
FIRST_ROLE=$(echo "$API_RESPONSE" | jq -r '.user.roles[0]')
echo "Birincil rol: $FIRST_ROLE"

# Tüm rolleri listeleme
echo "Tüm roller:"
echo "$API_RESPONSE" | jq -r '.user.roles[]'

Burada -r parametresi raw output anlamına geliyor. Bu parametre olmadan jq, string değerleri tırnak işaretleriyle döndürür. Bash script’lerinde genellikle -r kullanmak isteyeceksiniz.

Gerçek API Çağrıları ve curl Entegrasyonu

Teoriden pratiğe geçelim. Gerçek bir API çağrısını curl ile yapıp yanıtı işleyelim:

#!/bin/bash
# GitHub API'den repository bilgisi çekme

GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
REPO_OWNER="mycompany"
REPO_NAME="myproject"

# API çağrısını yap ve yanıtı değişkende sakla
API_RESPONSE=$(curl -s 
  -H "Authorization: token $GITHUB_TOKEN" 
  -H "Accept: application/vnd.github.v3+json" 
  "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}")

# HTTP status code'u ayrı al
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" 
  -H "Authorization: token $GITHUB_TOKEN" 
  "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}")

echo "HTTP Status: $HTTP_STATUS"

# Yanıttan bilgi çıkar
REPO_DESC=$(echo "$API_RESPONSE" | jq -r '.description')
STAR_COUNT=$(echo "$API_RESPONSE" | jq -r '.stargazers_count')
DEFAULT_BRANCH=$(echo "$API_RESPONSE" | jq -r '.default_branch')

echo "Açıklama: $REPO_DESC"
echo "Yıldız: $STAR_COUNT"
echo "Ana branch: $DEFAULT_BRANCH"

Yukarıdaki örnekte iki ayrı curl çağrısı var, bu verimsiz. Daha iyi bir yöntem:

#!/bin/bash
# Tek curl çağrısıyla hem body hem status code alma

TEMP_FILE=$(mktemp)

HTTP_STATUS=$(curl -s 
  -H "Authorization: token $GITHUB_TOKEN" 
  -H "Accept: application/vnd.github.v3+json" 
  -o "$TEMP_FILE" 
  -w "%{http_code}" 
  "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}")

API_RESPONSE=$(cat "$TEMP_FILE")
rm -f "$TEMP_FILE"

echo "HTTP Status: $HTTP_STATUS"
echo "Response body mevcut: $(echo "$API_RESPONSE" | wc -c) byte"

Bu yaklaşım daha temiz. Tek bir network isteğiyle hem body’yi hem status code’u alıyoruz.

HTTP Hata Kodlarını Yönetme

Sysadmin olarak en çok başımıza gelen sorun şu: script çalışıyor gibi görünüyor ama aslında API hata döndürüyor ve biz bunu fark etmiyoruz. Kapsamlı bir hata yönetimi fonksiyonu yazalım:

#!/bin/bash
# Kapsamlı API hata yönetimi

API_BASE_URL="https://api.example.com"
API_KEY="your-api-key-here"
LOG_FILE="/var/log/api_client.log"

log() {
    local level=$1
    shift
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
}

make_api_request() {
    local endpoint=$1
    local method=${2:-GET}
    local body=${3:-}

    local temp_file
    temp_file=$(mktemp)

    local curl_opts=(
        -s
        -X "$method"
        -H "Authorization: Bearer $API_KEY"
        -H "Content-Type: application/json"
        -H "Accept: application/json"
        -o "$temp_file"
        -w "%{http_code}"
        --connect-timeout 10
        --max-time 30
    )

    if [[ -n "$body" ]]; then
        curl_opts+=(-d "$body")
    fi

    local http_status
    http_status=$(curl "${curl_opts[@]}" "${API_BASE_URL}${endpoint}")
    local curl_exit_code=$?

    local response_body
    response_body=$(cat "$temp_file")
    rm -f "$temp_file"

    # curl'ün kendisinde hata var mı?
    if [[ $curl_exit_code -ne 0 ]]; then
        log "ERROR" "curl başarısız oldu. Exit code: $curl_exit_code, Endpoint: $endpoint"
        case $curl_exit_code in
            6)  log "ERROR" "DNS çözümlenemedi: $API_BASE_URL" ;;
            7)  log "ERROR" "Bağlantı reddedildi" ;;
            28) log "ERROR" "Timeout oluştu (30s aşıldı)" ;;
            *)  log "ERROR" "Bilinmeyen curl hatası" ;;
        esac
        return 1
    fi

    # HTTP status code kontrolü
    case $http_status in
        200|201|202|204)
            log "INFO" "Başarılı: $method $endpoint ($http_status)"
            echo "$response_body"
            return 0
            ;;
        400)
            log "ERROR" "Bad Request (400): Gönderilen veri hatalı"
            local error_msg
            error_msg=$(echo "$response_body" | jq -r '.message // .error // "Bilinmeyen hata"')
            log "ERROR" "Sunucu mesajı: $error_msg"
            return 2
            ;;
        401)
            log "ERROR" "Unauthorized (401): API key geçersiz veya süresi dolmuş"
            return 3
            ;;
        403)
            log "ERROR" "Forbidden (403): Bu işlem için yetkiniz yok"
            return 4
            ;;
        404)
            log "ERROR" "Not Found (404): $endpoint bulunamadı"
            return 5
            ;;
        429)
            log "WARN" "Rate Limit (429): Çok fazla istek gönderildi"
            local retry_after
            retry_after=$(echo "$response_body" | jq -r '.retry_after // 60')
            log "WARN" "${retry_after} saniye bekleyip tekrar deneyin"
            return 6
            ;;
        5*)
            log "ERROR" "Sunucu hatası ($http_status): API tarafında sorun var"
            return 7
            ;;
        *)
            log "ERROR" "Beklenmedik HTTP status: $http_status"
            return 8
            ;;
    esac
}

Bu fonksiyonu gerçek bir senaryoda şöyle kullanırsın:

# Kullanım örneği
response=$(make_api_request "/v1/users/1234")
exit_code=$?

if [[ $exit_code -eq 0 ]]; then
    user_name=$(echo "$response" | jq -r '.name')
    user_email=$(echo "$response" | jq -r '.email')
    log "INFO" "Kullanıcı bulundu: $user_name ($user_email)"
else
    log "ERROR" "Kullanıcı bilgisi alınamadı, exit code: $exit_code"
    exit 1
fi

JSON Doğrulama ve Null Değer Kontrolü

API yanıtı bazen beklediğimiz alanı içermeyebilir. Bu durumu handle etmeden jq çıktısını kullanan kod ciddi sorunlara yol açabilir:

#!/bin/bash
# JSON doğrulama ve null kontrolü

validate_json_response() {
    local response=$1

    # Önce geçerli JSON mı kontrol et
    if ! echo "$response" | jq -e . > /dev/null 2>&1; then
        echo "HATA: Geçersiz JSON yanıtı alındı"
        echo "Ham yanıt: $response"
        return 1
    fi

    return 0
}

safe_jq_extract() {
    local json=$1
    local path=$2
    local default_value=${3:-""}

    local value
    value=$(echo "$json" | jq -r "$path // empty")

    if [[ -z "$value" ]]; then
        echo "$default_value"
    else
        echo "$value"
    fi
}

# Örnek kullanım
API_RESPONSE='{"user": {"name": "Mehmet", "age": null, "department": ""}}'

validate_json_response "$API_RESPONSE" || exit 1

USERNAME=$(safe_jq_extract "$API_RESPONSE" '.user.name' "Bilinmeyen")
USER_AGE=$(safe_jq_extract "$API_RESPONSE" '.user.age' "0")
DEPARTMENT=$(safe_jq_extract "$API_RESPONSE" '.user.department' "Atanmamış")

echo "İsim: $USERNAME"
echo "Yaş: $USER_AGE"
echo "Departman: $DEPARTMENT"

// empty sözdizimi önemli. jq‘da // operatörü “alternatif” operatörüdür. Değer null veya false ise sağ taraftaki değeri döndürür. empty ise hiçbir çıktı üretmez, böylece bash tarafında boş string olarak yakalanır.

Rate Limiting ve Retry Mekanizması

Production ortamında API’lere istek atarken rate limiting meselesi kaçınılmaz olarak karşınıza çıkar. Exponential backoff stratejisi ile retry mekanizması kurmalısınız:

#!/bin/bash
# Retry ve exponential backoff implementasyonu

MAX_RETRIES=5
INITIAL_WAIT=1
MAX_WAIT=60

api_request_with_retry() {
    local endpoint=$1
    local method=${2:-GET}
    local body=${3:-}

    local retry_count=0
    local wait_time=$INITIAL_WAIT

    while [[ $retry_count -le $MAX_RETRIES ]]; do

        if [[ $retry_count -gt 0 ]]; then
            echo "Deneme $retry_count/$MAX_RETRIES - ${wait_time}s bekleniyor..."
            sleep "$wait_time"
            # Exponential backoff: bekleme süresini ikiye katla
            wait_time=$((wait_time * 2))
            # Maksimum bekleme süresini aşma
            [[ $wait_time -gt $MAX_WAIT ]] && wait_time=$MAX_WAIT
        fi

        local temp_file
        temp_file=$(mktemp)

        local http_status
        http_status=$(curl -s 
            -X "$method" 
            -H "Authorization: Bearer $API_KEY" 
            -H "Content-Type: application/json" 
            -o "$temp_file" 
            -w "%{http_code}" 
            --connect-timeout 10 
            --max-time 30 
            "${body:+-d $body}" 
            "${API_BASE_URL}${endpoint}")

        local curl_exit=$?
        local response
        response=$(cat "$temp_file")
        rm -f "$temp_file"

        # Başarılı yanıt
        if [[ $http_status -ge 200 && $http_status -lt 300 ]]; then
            echo "$response"
            return 0
        fi

        # Rate limit - Retry-After header'ına göre bekle
        if [[ $http_status -eq 429 ]]; then
            local retry_after
            retry_after=$(echo "$response" | jq -r '.retry_after // 0')
            if [[ $retry_after -gt 0 ]]; then
                echo "Rate limit aşıldı. Sunucu $retry_after saniye beklemesini istiyor."
                wait_time=$retry_after
            fi
        fi

        # 4xx hatalar genellikle retry etmeye değmez (401, 403, 404)
        if [[ $http_status -ge 400 && $http_status -lt 500 && $http_status -ne 429 ]]; then
            echo "HATA: $http_status - Retry yapılmayacak"
            echo "$response"
            return 1
        fi

        # 5xx ve network hataları için retry
        retry_count=$((retry_count + 1))
        echo "UYARI: HTTP $http_status alındı, tekrar deneniyor ($retry_count/$MAX_RETRIES)"

    done

    echo "HATA: Maksimum retry sayısına ulaşıldı ($MAX_RETRIES)"
    return 1
}

Gerçek Dünya Senaryosu: Monitoring Entegrasyonu

Şimdi her şeyi bir araya getirelim. Birçok şirkette Alertmanager veya PagerDuty gibi sistemlerle entegrasyon yapmak gerekiyor. Aşağıdaki script, bir monitoring API’sinden kritik uyarıları çekip Slack’e bildirim gönderiyor:

#!/bin/bash
# Prometheus Alertmanager API entegrasyonu ve Slack bildirimi

ALERTMANAGER_URL="http://alertmanager.internal:9093"
SLACK_WEBHOOK_URL="https://hooks.slack.com/services/xxx/yyy/zzz"
LOG_FILE="/var/log/alert_notifier.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}

# Aktif alertleri çek
fetch_active_alerts() {
    local response
    local http_status

    local temp_file
    temp_file=$(mktemp)

    http_status=$(curl -s 
        -o "$temp_file" 
        -w "%{http_code}" 
        --connect-timeout 5 
        --max-time 15 
        "${ALERTMANAGER_URL}/api/v2/alerts?active=true&silenced=false")

    response=$(cat "$temp_file")
    rm -f "$temp_file"

    if [[ $http_status -ne 200 ]]; then
        log "ERROR: Alertmanager erişilemedi. HTTP: $http_status"
        return 1
    fi

    # JSON doğrulama
    if ! echo "$response" | jq -e . > /dev/null 2>&1; then
        log "ERROR: Geçersiz JSON yanıtı"
        return 1
    fi

    echo "$response"
}

# Kritik alertları filtrele ve Slack mesajı oluştur
process_alerts() {
    local alerts_json=$1

    # Kritik severity'deki alertları say
    local critical_count
    critical_count=$(echo "$alerts_json" | jq '[.[] | select(.labels.severity == "critical")] | length')

    if [[ $critical_count -eq 0 ]]; then
        log "INFO: Kritik alert yok"
        return 0
    fi

    log "WARN: $critical_count adet kritik alert tespit edildi"

    # Her kritik alert için mesaj oluştur
    local slack_text="*:red_circle: $critical_count Kritik Alert Tespit Edildi*n"

    while IFS= read -r alert; do
        local alert_name
        local instance
        local description
        local started_at

        alert_name=$(echo "$alert" | jq -r '.labels.alertname // "Bilinmeyen"')
        instance=$(echo "$alert" | jq -r '.labels.instance // "N/A"')
        description=$(echo "$alert" | jq -r '.annotations.description // .annotations.summary // "Açıklama yok"')
        started_at=$(echo "$alert" | jq -r '.startsAt')

        slack_text+="• *$alert_name* - Instance: `$instance`n"
        slack_text+="  $descriptionn"
        slack_text+="  Başlangıç: $started_atnn"

        log "CRITICAL ALERT: $alert_name on $instance"
    done < <(echo "$alerts_json" | jq -c '.[] | select(.labels.severity == "critical")')

    # Slack'e gönder
    local slack_payload
    slack_payload=$(jq -n --arg text "$slack_text" '{
        "text": $text,
        "mrkdwn": true
    }')

    local slack_response
    local slack_status

    local temp_file
    temp_file=$(mktemp)

    slack_status=$(curl -s 
        -X POST 
        -H "Content-Type: application/json" 
        -d "$slack_payload" 
        -o "$temp_file" 
        -w "%{http_code}" 
        "$SLACK_WEBHOOK_URL")

    slack_response=$(cat "$temp_file")
    rm -f "$temp_file"

    if [[ $slack_status -eq 200 ]]; then
        log "INFO: Slack bildirimi gönderildi"
    else
        log "ERROR: Slack bildirimi gönderilemedi. HTTP: $slack_status, Response: $slack_response"
    fi
}

# Ana akış
main() {
    log "INFO: Alert kontrolü başlatılıyor"

    local alerts
    if ! alerts=$(fetch_active_alerts); then
        log "ERROR: Alert verisi alınamadı, çıkılıyor"
        exit 1
    fi

    process_alerts "$alerts"
    log "INFO: Alert kontrolü tamamlandı"
}

main "$@"

JSON Array’lerini İşleme ve Döngüler

Çok kayıtlı API yanıtlarını işlemek başlı başına bir konu. Sayfalama (pagination) desteği olan bir API’den tüm kayıtları çeken bir örnek:

#!/bin/bash
# Paginated API response işleme

API_URL="https://api.example.com"
API_KEY="your-key"

fetch_all_users() {
    local page=1
    local per_page=100
    local all_users="[]"
    local has_more=true

    while [[ "$has_more" == "true" ]]; do
        echo "Sayfa $page çekiliyor..."

        local response
        local temp_file
        temp_file=$(mktemp)

        local http_status
        http_status=$(curl -s 
            -H "Authorization: Bearer $API_KEY" 
            -o "$temp_file" 
            -w "%{http_code}" 
            "${API_URL}/v1/users?page=${page}&per_page=${per_page}")

        response=$(cat "$temp_file")
        rm -f "$temp_file"

        if [[ $http_status -ne 200 ]]; then
            echo "HATA: HTTP $http_status alındı"
            return 1
        fi

        # Bu sayfadaki kayıt sayısını kontrol et
        local page_count
        page_count=$(echo "$response" | jq '.data | length')

        if [[ $page_count -eq 0 ]]; then
            has_more=false
            echo "Tüm sayfalar işlendi"
            break
        fi

        # Bu sayfanın kullanıcılarını genel listeye ekle
        all_users=$(echo "$all_users $response" | jq -s '.[0] + .[1].data')

        # Sonraki sayfa var mı?
        local next_page
        next_page=$(echo "$response" | jq -r '.meta.next_page // empty')

        if [[ -z "$next_page" ]]; then
            has_more=false
        else
            page=$((page + 1))
        fi
    done

    echo "$all_users"
}

# Tüm kullanıcıları çek ve işle
ALL_USERS=$(fetch_all_users)
TOTAL=$(echo "$ALL_USERS" | jq 'length')
echo "Toplam $TOTAL kullanıcı çekildi"

# Sadece aktif kullanıcıları filtrele
ACTIVE_USERS=$(echo "$ALL_USERS" | jq '[.[] | select(.status == "active")]')
ACTIVE_COUNT=$(echo "$ACTIVE_USERS" | jq 'length')
echo "Aktif kullanıcı sayısı: $ACTIVE_COUNT"

# Admin kullanıcıların email listesini çıkar
echo "Admin kullanıcılar:"
echo "$ALL_USERS" | jq -r '.[] | select(.role == "admin") | .email'

Güvenlik Konuları

API ile çalışırken güvenlik göz ardı edilemez:

  • API anahtarlarını asla script içine yazmayın: Environment variable veya secrets manager kullanın. export API_KEY=$(vault kv get -field=key secret/api) gibi bir yaklaşım benimseyin.
  • Log dosyalarına hassas veri yazmayın: Response body’yi loglamadan önce token, password gibi alanları maskeleyin. jq 'del(.password, .token, .secret)' ile bu alanları temizleyebilirsiniz.
  • SSL doğrulamasını devre dışı bırakmayın: Test ortamında bile -k veya --insecure kullanmak kötü alışkanlık. Bunun yerine kendi CA sertifikanızı --cacert ile belirtin.
  • Timeout değerlerini mutlaka ayarlayın: --connect-timeout ve --max-time olmadan bir API cevap vermezse script’iniz sonsuza kadar bekler.
  • Input validasyonu yapın: API’den gelen veriler shell’e geçmeden önce sanitize edin. Özellikle komut oluşturmada kullanılacak değerleri çift tırnak içinde kullanın.

Sonuç

API yanıtlarını düzgün işlemek sysadmin işinin ayrılmaz bir parçası haline geldi. Özetlemek gerekirse:

  • jq senin en iyi arkadaşın, özellikle -r ve // empty kombinasyonunu öğren
  • Her zaman HTTP status code’u kontrol et, yanıtın body’sini değil
  • curl hataları ile HTTP hataları birbirinden farklı, ikisini ayrı handle et
  • Retry mekanizması kur ama 4xx hataları için retry etme, boşuna kaynak harcamazsın
  • Paginated endpoint’lerde tüm veriyi çektiğinden emin ol
  • Temp file kullan, büyük response’ları değişkende tutmak bellek sorunlarına yol açabilir
  • Null ve boş değerlere karşı savunmacı kod yaz, API’ler her zaman beklediğin alanı döndürmez

Bu yazıdaki örnekleri kendi ortamınıza uyarlayabilirsiniz. Önemli olan mantığı kavramak; hangi API olursa olsun, hata yönetimi ve veri doğrulama prensipleri değişmiyor.

Bir yanıt yazın

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