Sunucuda idempotency'yi kurdun: anahtar saklanıyor, aynı anahtar gelince cevap tekrar dönüyor, kuyrukta consumer tekrarı tanıyor. Sonra istemciye baktın; ödeme butonuna her basışta yeni bir Idempotency-Key üretiliyor. Kullanıcı iki kez bastı, iki farklı anahtar gitti, sunucu ikisini de ayrı ödeme sandı. Bütün o altyapı hiçbir şeyi korumadı.
Idempotency key, istemcinin ürettiği ve sunucunun aynı işlemin tekrarını tanımasını sağlayan benzersiz anahtardır. Kısa cevap: butona basışta değil, ödeme niyeti oluştuğu anda, yani ödeme ekranı açılırken üretilir.
Bunu Idempotency yazısının sonunda tek cümleyle geçmiştim. Bu kısa not o cümlenin devamı; çünkü "nerede üretilir" sorusunun cevabı bizde de ilk başta göründüğü kadar basit çıkmadı.
Key bir isteğin değil, bir niyetin kimliği
Idempotency key, istemcinin "ben bu ödemeyi yapmak istiyorum" dediği anı temsil eden benzersiz bir değerdir. Tanımdaki önemli kelime istek değil, niyet. Bir niyet için kaç istek giderse gitsin (çift tıklama, timeout sonrası retry, ağ kopması) hepsi aynı anahtarı taşır. Sunucunun tekrarı tanıması ancak böyle mümkün olur; Stripe'ın dokümanı da anahtarı aynı mantıkla tanımlar.
Bu yüzden anahtar butonun tıklama olayında değil, niyetin doğduğu yerde üretilir. Bizde bu yer ödeme ekranının açılışı. Kullanıcı sepetini onaylayıp ödeme ekranına geldiğinde sipariş artık değişmiyor; ekran açılırken üretilen tek anahtar o siparişin bütün denemelerinde aynı kalıyor.
Ekran değişebiliyorsa
Her ödeme ekranı donmuş değil. Tutarın, taksit seçeneğinin ya da ödeme yönteminin ekranda değişebildiği akışlar var. Orada "ekran açılışı" yanlış an olur: kullanıcı taksidi değiştirir, aynı anahtarla farklı bir istek gider.
Bu durumda anahtar, ödemenin snapshot'ı alındığı anda üretilir. Snapshot, kullanıcının gördüğü ve onayladığı son halin kaydıdır; tutar ve seçenekler o anda sabitlenir, anahtar o kayda bağlanır. Sonrasında bir şey değişirse yeni bir snapshot ve yeni bir anahtar doğar.
Sunucu tarafı bu kuralın bekçisi. Aynı anahtar farklı bir istek gövdesiyle gelirse bu bir tekrar değil, istemci hatasıdır ve 422 döner. İlk yazıdaki dört durumdan biri buydu; istemci anahtarı doğru yerde üretmezse en çok bu kapıya çarpar.
Sayfa yenilenince ne olur
Anahtar istemcide bellekte duruyor. Kullanıcı sayfayı yenilerse ya da geri dönüp tekrar gelirse anahtar gider; ekran yeniden açılır ve yeni bir anahtar üretilir. İlk bakışta bu, yenilemeyle çift ödemeye kapı açıyor gibi görünür.
Bizde bunu anahtar değil, başka bir soru engelliyor. Ödeme ekranı açılırken istemci sunucuya "bu sipariş ödendi mi?" diye soruyor. İlk deneme sonuçlandıysa ekran ödeme formunu değil sonucu gösteriyor; sonuçlanmadıysa kullanıcı yeni anahtarla devam ediyor. Yani anahtar aynı niyetin tekrarlarını birbirine bağlıyor, sipariş durumu sorgusu ise niyetin ikinci kez doğmasını engelliyor. İki ayrı koruma, iki ayrı iş.
Anahtarı sessionStorage'da saklayıp yenilemeden sonra da aynı anahtarla devam etmek mümkün. Bunun bedeli, saklanan anahtarın ait olduğu siparişle hâlâ eşleşip eşleşmediğini ayrıca takip etmek. Sipariş durumunu zaten soruyorsan bu ek karmaşıklık pek bir şey kazandırmıyor; biz o yüzden saklamadık.
Kodda nasıl görünüyor
export class PaymentScreen {
// Niyet başına bir anahtar: ekran açılırken (ya da snapshot anında) üretilir.
private idempotencyKey = crypto.randomUUID();
constructor(private readonly api: PaymentApi, private readonly orderId: string) {}
async open() {
const status = await this.api.getOrderStatus(this.orderId);
if (status.isPaid) return this.showResult(status);
this.showForm();
}
// Çift tıklama, timeout, retry: hepsi aynı anahtarla gider.
async pay(amount: Money) {
return this.api.charge(this.orderId, amount, { idempotencyKey: this.idempotencyKey });
}
}
Anahtarın üretildiği satır pay metodunun içinde olsaydı her tıklamada yenilenirdi; sınıfın alanı olunca ekranın ömrü boyunca sabit kalıyor. Başlık adı için IETF'nin Idempotency-Key taslağı var; biz de aynı adı kullanıyoruz.
Kendi sisteminde nasıl fark edersin
Ödeme ekranında butona hızlıca iki kez bas ve ağ sekmesinde iki isteğin Idempotency-Key başlığına bak. Değerler farklıysa anahtar yanlış yerde üretiliyor; sunucudaki idempotency katmanı o iki isteği birbirine bağlayamaz.
Okura kalan
- Anahtar niyet başına bir tane; isteğe değil, ödeme niyetine bağla.
- Ekran değişebiliyorsa anahtar snapshot anında doğar, aynı anahtarla farklı istek
422ile reddedilir. - Sayfa yenilemesine karşı korumayı anahtara yükleme; ekran açılırken sipariş durumunu sor.
Bu yazı ilk olarak huseyinbozdemir.com'da yayımlandı.
Top comments (0)