Ana içeriğe geç
Kesit Python
Orta · Otomasyon, web ve araçlar

HTTP ve REST API istemcisi: requests, httpx ve dayanıklı istekler

HTTP isteğinin anatomisini, metotları ve durum kodlarını öğrenir; requests/httpx ile kimlik doğrulamalı, zaman aşımlı, sayfalı ve hız sınırına saygılı API istemcisi yazar, sahte taşıyıcıyla test edersin.

Uygulamada aç, 18 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Neden önemli?

Anadolu Market'in sipariş sistemi kargo firmasının API'sinden teslimat durumlarını, Ilgaz Bank'ın ödeme API'sinden tahsilat sonuçlarını, merkez bankasının servisinden döviz kurlarını çeker. Modern yazılımda sistemler birbirleriyle çoğunlukla HTTP üzerinden konuşan API'lerle (Application Programming Interface) haberleşir. Bir API istemcisi yazmak kolaydır; dayanıklı bir istemci yazmak ise ayrı bir beceridir: ağ kopar, sunucu yavaşlar, hız sınırına takılırsın, beklenmedik JSON gelir. Bu derste ikisini de öğreneceksin.

Önceki derste HTML kazımayı gördün; API ise verinin makine için hazırlanmış, kurallı halidir. API varsa kazımaya gerek yoktur. Sunucu tarafını (FastAPI ile uç nokta yazmayı) ileri seviyedeki "API ve web" dersinde göreceksin; burada istemci tarafındayız.

HTTP isteğinin anatomisi

POST /v1/siparisler?kaynak=mobil HTTP/1.1          ← metot, yol, sorgu parametresi
Host: api.anadolumarket.example
Authorization: Bearer eyJhbGciOi...                 ← kimlik doğrulama başlığı
Content-Type: application/json                      ← gövdenin biçimi
Accept: application/json                            ← beklediğin yanıt biçimi

{"musteri_id": 1042, "urunler": [{"sku": "AM-101", "adet": 2}]}   ← gövde (body)

Yanıt da benzer yapıdadır: bir durum kodu (status code), başlıklar ve gövde. REST tarzı API'lerde adres bir kaynağı (resource: siparişler, müşteriler), metot ise o kaynağa ne yapılacağını anlatır.

MetotAmaçGövdeGüvenliIdempotent
GETKaynağı okuYokEvetEvet
POSTYeni kaynak oluştur / işlem başlatVarHayırHayır
PUTKaynağı tamamen değiştirVarHayırEvet
PATCHKaynağın bir kısmını güncelleVarHayırGenelde hayır
DELETEKaynağı silGenelde yokHayırEvet

Güvenli (safe) metot sunucuda bir şey değiştirmez. Idempotent metodu aynı istekle bir kez de on kez de çağırsan sonuç aynıdır. Bu ayrım yeniden denemede hayati önem taşır: GET'i tekrar denemek zararsızdır, ama zaman aşımına uğrayan bir ödeme POST'unu körlemesine tekrarlarsan müşteriden iki kez para çekebilirsin.

Durum kodları

KodAnlamıİstemci ne yapmalı?
200 OKBaşarılı, gövdede veri varJSON'u oku
201 CreatedKaynak oluşturulduYeni kaynağın kimliğini ya da Location başlığını al
204 No ContentBaşarılı, gövde yok.json() çağırma
301 / 302 / 304Taşındı / geçici yönlendirme / değişmediKütüphane yönlendirmeyi izler; 304'te önbelleği (cache) kullan
400 Bad Requestİstek hatalı (eksik alan, yanlış biçim)Tekrar deneme; isteği düzelt
401 UnauthorizedKimlik doğrulanamadı (anahtar yok/yanlış/süresi dolmuş)Token'ı yenile ya da yapılandırmayı düzelt
403 ForbiddenKim olduğun belli ama yetkin yokTekrar deneme; yetki iste
404 Not FoundKaynak yokÇoğu zaman "yok" olarak işle (None)
409 / 422Çakışma / doğrulama hatasıGövdedeki hata ayrıntısını kullanıcıya göster
429 Too Many RequestsHız sınırı aşıldıRetry-After kadar bekle, sonra tekrar dene
500 / 502 / 503 / 504Sunucu ya da ağ geçidi sorunuGeri çekilerek sınırlı sayıda tekrar dene

requests ile istek atmak

requests Python'un en yaygın HTTP kütüphanesidir. Tarayıcı motorunda ağ olmadığı için bu kodu kendi bilgisayarında dene.

import os
import requests

TABAN = "https://api.anadolumarket.example/v1"
token = os.environ["ANADOLU_API_TOKEN"]          # anahtar kodda değil, ortamda

with requests.Session() as oturum:                # bağlantıyı yeniden kullanır, ortak başlık
    oturum.headers.update({"Authorization": f"Bearer {token}", "Accept": "application/json"})

    y = oturum.get(f"{TABAN}/siparisler", params={"sehir": "İzmir", "sayfa": 1}, timeout=10)
    y.raise_for_status()                          # 4xx/5xx → requests.HTTPError
    for s in y.json()["veri"]:
        print(s["id"], s["tutar"])

    yeni = oturum.post(f"{TABAN}/siparisler", json={"musteri_id": 1042, "tutar": 349.9}, timeout=(3, 10))
    print(yeni.status_code, yeni.headers.get("Location"))
Kendi bilgisayarında denepip install requests httpx. Gerçek bir API ile denemek için herkese açık test servisleri (ör. httpbin) kullanabilirsin. httpx tarayıcı motorunda da yüklü; ağ isteği atamaz ama birazdan göreceğin sahte taşıyıcıyla tam çalışır.

httpx ve sahte taşıyıcıyla test

httpx, requests'e çok benzeyen modern bir kütüphanedir; ek olarak async desteği ve HTTP/2 sunar. En büyük test kolaylığı MockTransport'tur: istemci gerçek ağ yerine senin yazdığın bir fonksiyona istek gönderir. Böylece istemci kodun hiç değişmeden test edilir. Bu derste alıştırmaların çoğu bu yöntemi kullanıyor.

import httpx

def sahte_sunucu(istek: httpx.Request) -> httpx.Response:
    if istek.headers.get("Authorization") != "Bearer gizli-123":
        return httpx.Response(401, json={"hata": "yetkisiz"})
    if istek.url.path == "/v1/siparisler/AM-7":
        return httpx.Response(200, json={"id": "AM-7", "durum": "kargoda"})
    return httpx.Response(404, json={"hata": "bulunamadı"})

istemci = httpx.Client(
    transport=httpx.MockTransport(sahte_sunucu),
    base_url="https://api.anadolumarket.example/v1",
    headers={"Authorization": "Bearer gizli-123"},
    timeout=5.0,
)
y = istemci.get("/siparisler/AM-7")
print(y.status_code, y.json())
y = istemci.get("/siparisler/AM-999")
print(y.status_code, y.is_success)
try:
    y.raise_for_status()
except httpx.HTTPStatusError as e:
    print("Hata:", e.response.status_code, e.request.url)

Sorgu parametreleri ve kimlik doğrulama

Sorgu parametreleri adresin ? sonrasındaki anahtar=değer çiftleridir. Özel karakterler yüzde kodlamasıyla (percent-encoding) yazılır; urlencode bunu senin yerine yapar.

from urllib.parse import urlencode
import base64

params = {"sehir": "İzmir", "q": "çay & şeker", "sayfa": 2, "kategori": None}
temiz = {k: v for k, v in params.items() if v is not None}
print(urlencode(temiz))

kullanici, parola = "rapor_bot", "s3cr3t"
kod = base64.b64encode(f"{kullanici}:{parola}".encode()).decode()
print("Authorization: Basic", kod)
YöntemNasıl gönderilir?Not
API anahtarıX-API-Key: abc123 başlığı (bazen ?api_key=)Başlık tercih edilir; adres günlüklere düşer
Bearer tokenAuthorization: Bearer <token>OAuth2/JWT; süresi dolar, yenilenir
BasicAuthorization: Basic base64(kullanıcı:parola)Base64 şifreleme değildir; yalnız HTTPS ile
OAuth2 client credentialsÖnce token uç noktasından token alınırKurumsal servisler arası yaygın yöntem

Gizli anahtarlar: kodda değil, ortamda

API anahtarını koda yazıp Git'e göndermek en sık ve en pahalı hatalardan biridir: depo bir kez paylaşıldığında anahtar sızmış sayılır ve iptal edilmesi gerekir. Anahtarlar ortam değişkeninde (environment variable) ya da bir gizli anahtar yöneticisinde (secret manager) tutulur. Yerelde .env dosyası kullanıyorsan onu .gitignore'a ekle.

import os

def token_al(ortam=os.environ):
    token = ortam.get("ANADOLU_API_TOKEN")
    if not token:
        raise RuntimeError("ANADOLU_API_TOKEN tanımlı değil; .env dosyanı ya da ortamını kontrol et")
    return token

def maskele(deger):
    return "****" + deger[-4:] if len(deger) > 4 else "****"

try:
    token_al({})
except RuntimeError as e:
    print("Hata:", e)
t = token_al({"ANADOLU_API_TOKEN": "tok_live_8f3a91c2"})
print("Kullanılan token:", maskele(t))
Günlükte sır olmazİstek başlıklarını hata ayıklamak için yazdırırken Authorization, X-API-Key, Cookie değerlerini maskele. Günlük sistemleri çoğu zaman uygulamadan çok daha fazla kişinin erişimine açıktır.

Sayfalama

API'ler büyük listeleri parça parça verir. Üç yaygın biçim vardır ve istemcinin döngüsü buna göre kurulur:

BiçimİstekDurma koşulu
Sayfa numarası?sayfa=3&boyut=100Boş liste ya da sayfa == toplam_sayfa
Offset/limit?offset=200&limit=100Dönen kayıt sayısı < limit
İmleç (cursor)?imlec=eyJpZCI6...Yanıttaki sonraki_imlec boş
Link başlığıYanıtta Link: <...>; rel="next"rel="next" yok

İmleç tabanlı sayfalama, veri arada değişse bile kayıt atlamaz ya da tekrarlamaz; büyük ve canlı veri için en sağlam yöntemdir. Her durumda bir en fazla sayfa sınırı koymak, hatalı bir API'nin istemcini sonsuz döngüye sokmasını önler.

Hız sınırı ve üstel geri çekilme

Çoğu API dakikada belli sayıda isteğe izin verir; aşarsan 429 döner ve çoğu zaman Retry-After başlığıyla kaç saniye beklemen gerektiğini söyler. Geçici sunucu hatalarında (502, 503, 504) ise üstel geri çekilme (exponential backoff) uygulanır: 1 sn, 2 sn, 4 sn, 8 sn… Çok sayıda istemci aynı anda tekrar denemesin diye bekleme süresine küçük bir rastgelelik (jitter) eklenir.

import random

def bekleme(deneme, retry_after=None, taban=1.0, tavan=30.0, rng=random.Random(0)):
    if retry_after is not None and str(retry_after).isdigit():
        return float(retry_after)
    ust = min(tavan, taban * 2 ** deneme)
    return round(rng.uniform(0, ust), 2)        # "full jitter"

for i in range(6):
    print(i, bekleme(i))
print("Retry-After ile:", bekleme(0, retry_after="7"))
  1. Yalnızca geçici hataları tekrar dene: zaman aşımı, bağlantı hatası, 408, 429, 502, 503, 504.
  2. 400, 401, 403, 404, 422 gibi istemci hatalarını tekrar deneme; aynı istek aynı sonucu verir.
  3. Deneme sayısını sınırla (genelde 3–5) ve son denemeden sonra hatayı yukarı fırlat; sessizce None döndürme.
  4. Idempotent olmayan istekleri (POST) yalnızca sunucu destekliyorsa bir Idempotency-Key başlığıyla tekrar dene.
  5. Her denemeyi günlüğe yaz: hangi adres, hangi kod, ne kadar beklendi.

Hata yönetimi ve yanıt doğrulama

requestshttpxNe zaman?
requests.Timeouthttpx.TimeoutExceptionSüre doldu
requests.ConnectionErrorhttpx.ConnectError (TransportError)DNS, bağlantı reddi, ağ kopması
requests.HTTPErrorhttpx.HTTPStatusErrorraise_for_status() 4xx/5xx gördü
requests.RequestExceptionhttpx.HTTPErrorHepsinin atası

2xx almak verinin doğru olduğu anlamına gelmez. Dış sistemden gelen her yanıtı sınırda doğrula: beklenen alanlar var mı, tipleri doğru mu, değerler mantıklı mı? Küçük işlerde elle kontrol, büyük işlerde pydantic modelleri kullanılır. Doğrulanmamış veri sistemin içine sızarsa hata çok daha uzakta ve pahalı bir yerde ortaya çıkar.

Gerçek işteBir sağlık randevu sisteminde hastane bilgi sisteminin API'si yoğun saatlerde 503 döner. İyi bir entegrasyon her isteğe zaman aşımı koyar, 503 ve 429'da geri çekilerek en fazla 4 kez dener, randevu oluşturma POST'unu her randevu için üretilen sabit bir Idempotency-Key ile gönderir (tekrar denemede çift randevu oluşmaz), tüm token'ları günlükte maskeler ve sağlayıcı tamamen çöktüğünde kullanıcıya "kaydınız sıraya alındı" deyip işi kuyruğa bırakır.
Uzmanlaş

İstemciyi sınıfa sar

Uygulamanın her yerinde httpx.get çağırmak yerine tek bir KargoAPI sınıfı yaz: taban adres, kimlik, zaman aşımı, yeniden deneme ve doğrulama bu sınıfta olsun; dışarıya gonderi_durumu(takip_no) gibi iş diliyle konuşan metotlar açsın. Testlerde sınıfa MockTransport'lu bir httpx.Client enjekte edersin.

Devre kesici (circuit breaker)

Karşı servis tamamen çöktüyse her isteği 4 kez denemek hem seni yavaşlatır hem onu daha çok yorar. Devre kesici, art arda belli sayıda hata görünce bir süre hiç istek atmadan hemen hata döner, sonra tek bir deneme isteğiyle servisin düzelip düzelmediğine bakar.

Önbellek ve koşullu istek

Sık değişmeyen veride (ürün kataloğu, il listesi) yanıtın ETag başlığını sakla, sonraki istekte If-None-Match gönder; değişmediyse sunucu gövdesiz 304 döner. Hem hız sınırını korursun hem bant genişliğini.

Async istemci

Yüzlerce bağımsız isteği sırayla atmak yavaştır. httpx.AsyncClient ve asyncio.gather ile eşzamanlı istek atabilirsin; ama eşzamanlılığı (concurrency) bir semafor ile sınırla ki hız sınırına çarpmayasın. Ayrıntıları ileri seviyedeki async dersinde göreceksin.

Alıştırmalara geç (18)