API'de idempotency: aynı isteği güvenle tekrar etmek
Tekrarlanan API isteklerinin yan etki üretmemesini tasarlamak; idempotency anahtarları ve uygulama katmanında pratik çözümler.
Şöyle bir senaryo düşünün: kullanıcı bir ödeme başlatıyor, istek yolda kayboluyor ya da istemci bir timeout alıyor. İstemci ne yapacak? Yeniden denemeli mi? Ödeme gerçekleşti mi gerçekleşmedi mi bilmiyor.
Bu belirsizlik, ağ üzerinden çalışan sistemlerin kaçınılmaz bir özelliğidir. İstek gönderildi ama yanıt alınamadı — bu, “işlem yapılmadı” anlamına gelmiyor. Belki işlem tamamlandı, yanıt dönmedi. Belki işlem yarıda kaldı. Belki hiç başlamadı.
Çözüm bu belirsizliği kabul edip, tekrarlanan isteğin güvenli olmasını tasarlamak. Bu kavrama idempotency (eşgüçlülük) deniyor.
Idempotency nedir?
Matematikte bir operasyon idempotent ise, aynı girdi ile kaç kez uygulanırsa uygulansın sonuç değişmez. API bağlamında: aynı isteği birden fazla göndermek, bir kez göndermekle aynı etkiyi yaratmalıdır.
Tanım HTTP’nin kendi spesifikasyonunda duruyor. RFC 9110’un ifadesiyle bir yöntem, “aynı isteğin birden çok kez gönderilmesinin sunucu üzerinde amaçlanan etkisi tek bir isteğinkiyle aynıysa” idempotenttir; spesifikasyonun tanımladığı yöntemler arasında PUT, DELETE ve güvenli (safe) yöntemler idempotenttir. HTTP yöntemleri bu açıdan farklı davranır:
- GET, HEAD, OPTIONS, PUT, DELETE — doğası gereği idempotent. Aynı
GET /users/5isteğini on kez gönderin; sonuç aynı. - POST — doğası gereği idempotent değil. Her
POST /paymentsyeni bir ödeme oluşturabilir.
Ayrım pratikte şu yüzden önemli: RFC, idempotent bir yöntemin “istemci yanıtı okuyamadan bağlantı koptuğunda otomatik olarak tekrarlanabileceğini” söylüyor, ama idempotent olmayan bir yöntem için istemcinin bunu kendiliğinden yapmaması gerektiğini ekliyor. Yani POST’ta yeniden denemeyi güvenli kılmak bizim işimiz.
Sorun genellikle POST ile ve durumu değiştiren işlemlerde ortaya çıkıyor.
Idempotency anahtarı
Çözümün özü şu: istemci, her benzersiz operasyon için bir key üretir ve bunu istekle birlikte gönderir. Sunucu bu anahtarı gördüğünde şunu yapar: “Bu anahtarla daha önce bir istek işledim mi? Evet ise, aynı yanıtı dönerim; hayır ise, işlemi yaparım ve sonucu bu anahtarla saklarım.”
İstemci aynı anahtarla yeniden denerse, işlem tekrar çalışmaz; daha önce üretilen yanıt döner.
POST /api/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 9900,
"currency": "TRY",
"method": "card"
}
Bu kalıbın en bilinen uygulaması Stripe. Kendi dokümanına göre anahtarı istemci üretiyor, Stripe anahtar için V4 UUID öneriyor ve anahtarlar en az 24 saat yaşlandıktan sonra sistemden silinebiliyor. Aynı yaklaşımı kendi API’lerinizde de uygulayabilirsiniz.
Laravel’de uygulama
Temel akış şu: istek geldiğinde önce önbellekte (veya veritabanında) bu anahtara karşılık bir kayıt var mı kontrol et; varsa saklı yanıtı dön; yoksa işlemi çalıştır, yanıtı sakla ve dön.
// app/Http/Middleware/IdempotencyMiddleware.php
class IdempotencyMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$key = $request->header('Idempotency-Key');
if (!$key) {
return $next($request);
}
$cacheKey = 'idempotency:' . auth()->id() . ':' . $key;
if ($cached = Cache::get($cacheKey)) {
return response()->json(
json_decode($cached['body'], true),
$cached['status']
);
}
$response = $next($request);
// 5xx dışındaki yanıtları sakla — 4xx dahil
if ($response->getStatusCode() < 500) {
Cache::put($cacheKey, [
'body' => $response->getContent(),
'status' => $response->getStatusCode(),
], now()->addHours(24));
}
return $response;
}
}
Birkaç not:
Kullanıcı bazlı anahtar. auth()->id() ile anahtarı kullanıcıya bağlıyorum. Farklı kullanıcıların aynı anahtarı göndermesi durumunda çakışma olmasın.
Hata yanıtlarını saklamak. Yukarıdaki kodda 5xx saklanmıyor; istemci yeniden deneyebilmeli. 4xx (istemci hatası) ise saklanıyor: aynı hatalı isteği yeniden gönderirseniz aynı hata döner.
Bu bilinçli bir tercih ve Stripe’ınkinden farklı: Stripe ilk isteğin durum kodunu ve gövdesini, istek başarılı olsun ya da olmasın saklıyor — aynı anahtarla gelen sonraki istekler 500 hatalarını bile aynen geri alıyor. Seçim, “yeniden deneme sunucu hatasını aşabilmeli mi” sorusuna verdiğiniz cevaba bağlı. Sunucuda geçici bir arıza bekliyorsanız benim yaptığım gibi 5xx’i saklamayın; yeniden denemenin kesinlikle yeni bir yan etki üretmemesini istiyorsanız Stripe’ın davranışı daha güvenli.
Saklama süresi. 24 saat makul bir başlangıç — Stripe da anahtarları en az bir gün yaşlandıktan sonra siliyor. İş gereksinimlerine göre ayarlanabilir.
Anahtarı kim üretir?
İstemci üretmeli. Sunucu üretirse anlam kalmaz: sunucu zaten işlemi yaptı, yanıtı döndü; anahtar sonradan üretilmiş oldu. Stripe da bunu böyle tarif ediyor: anahtarı istemci üretir, sunucu onu aynı isteğin sonraki denemelerini tanımak için kullanır. İstemcinin elinde bir UUID v4 ya da benzeri benzersiz bir tanımlayıcı olmalı ve bu tanımlayıcıyı yeniden deneme boyunca korumalı.
React Native tarafında:
import { randomUUID } from "expo-crypto";
async function createPayment(amount: number) {
const idempotencyKey = randomUUID();
try {
const response = await api.post(
"/payments",
{ amount },
{ headers: { "Idempotency-Key": idempotencyKey } }
);
return response.data;
} catch (error) {
if (isNetworkError(error)) {
// Aynı anahtarla yeniden dene
return retryWithSameKey(idempotencyKey, amount);
}
throw error;
}
}
Anahtar, ödeme nesnesi oluşturulduğunda üretilir; yeniden denemede aynı anahtar kullanılır. Sunucu ikinci isteği görünce zaten işlemi yapmış olduğunu anlayacak.
Sadece ödeme değil
Idempotency’nin değeri ödeme sistemleriyle sınırlı değil. Kullanıcı kaydı, e-posta gönderimi, stok rezervasyonu — kullanıcı için görünür bir yan etkisi olan her POST işlemi bu desenden yararlanabilir.
Özellikle mobil uygulamalarda ağ bağlantısı güvenilmez; kullanıcı “Gönder” butonuna birden fazla kez basabilir. Bu durumları ele almak için istemci tarafında çift gönderimi engellemek (buton devre dışı bırakmak) yeterli değil; sunucunun da hazır olması gerekir.
Tasarımı bu varsayım üzerine kurmak — “istek tekrar gelebilir” — daha dayanıklı bir API ortaya çıkarır.
Kaynaklar
Bu konudaki deneyler
Swipenor: skoru istemci hesaplarsa sunucu neye güvenir?
Kaydırmalı doğru/yanlış bilgi yarışması; asıl mesele oyun değil, istemcinin ürettiği skoru imzayla doğrulanabilir kılmak.
Şu an ne yapıyor
Kaydırmalı doğru/yanlış bilgi yarışması bir mobil uygulama olarak çekirdek döngüsüyle uçtan uca çalışıyor; skoru istemci üretse de server_nonce ve HMAC imzasıyla sunucu tarafında doğrulanıyor. swipenor.com uygulamanın tanıtım sayfasıdır, oyunun kendisi değil.
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.