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.
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.
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"
}
Stripe bu yaklaşımı uzun süredir kullanıyor; 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);
// Yalnızca başarılı yanıtları sakla
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. 5xx hataları saklanmıyor; istemci yeniden deneyebilmeli. 4xx (istemci hatası) ise saklanabilir: aynı hatalı isteği yeniden gönderseniz, aynı hata döner.
Saklama süresi. 24 saat makul bir başlangıç; iş 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. İ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.