- Konu sahibi
- #1
İki kez gönderimi engellemek için formun
Aşağıdaki örnekte form gönderildiğinde tarayıcının normal sayfa yenilemesi
Bu örneği bir HTML dosyasına koyup tarayıcıda açabilirsiniz. Gerçek uygulamada
Buradaki akış şöyledir: Kullanıcı formu gönderir, olay işleyicisi çalışır, ilk satırlarda ikinci gönderim kontrol edilir ve ardından istek başlatılır. İstek başarılıysa düğme devre dışı kalmaya devam eder; form temizlenir ve başarı mesajı gösterilir. İstek hata verirse kilit kaldırılır. Böylece kullanıcı, başarısız işlemden sonra yeniden deneyebilir.
Gönderme düğmesini devre dışı bırakmak, kullanıcının düğmeye art arda basmasını önler. Ancak bu kontrolü tek savunma olarak kullanmayın. Form, klavyeden Enter tuşuyla gönderilebilir; başka bir kod aynı form üzerinde
Birden fazla kod parçası aynı forma
Form doğrulamasını da gönderimden önce değil, gönderim akışının içinde düşünün.
Doğrulama başarısız olduğunda kullanıcı formu düzeltebilmelidir. Bu nedenle kilidi ancak veriler geçerli olduktan ve gerçekten ağ isteği başlatılmadan hemen önce etkinleştirin.
Bu sırada kullanıcı geçersiz e-posta girerse
En sık yapılan hatalardan biri, düğmeyi devre dışı bıraktıktan sonra yalnızca başarılı yanıtı ele almaktır. Ağ bağlantısı koparsa veya sunucu hata döndürürse düğme kilitli kalır ve kullanıcı tekrar deneyemez. Bu nedenle hata durumunda kilidi kaldırın.
İstek tamamlandığında kilidi her durumda kaldırmak istiyorsanız
Başarı mesajının görünmesi, sunucunun işlemi kabul ettiğini gösterir; kullanıcının aynı sayfada başka bir form göndermesini veya işlemin arka planda kesin olarak tamamlandığını tek başına kanıtlamaz. Uygulamanızda sunucu yanıtının içeriğini de kontrol edin. Örneğin sunucu bir kayıt kimliği döndürüyorsa bu kimliği kullanıcıya gösterilen sonuçla ilişkilendirebilir veya durum sorgusu başlatabilirsiniz.
Tarayıcı tarafındaki kilit, aynı sayfada hızlı ve yanlışlıkla yapılan tekrarları önler. Fakat HTTP isteği sunucuya ulaştıktan sonra tarayıcı kontrolü artık tek başına yeterli değildir. Kullanıcı sayfayı yenileyebilir, iki sekme açabilir, bağlantı zaman aşımından sonra aynı isteği yeniden gönderebilir veya isteği doğrudan oluşturabilir.
Bu nedenle para çekme, sipariş oluşturma, üyelik kaydı veya e-posta gönderme gibi yan etkili işlemlerde sunucu aynı işlemi ikinci kez uygulamamalıdır. Bunun yaygın yolu, istemcinin her mantıksal işlem için benzersiz bir istek anahtarı göndermesi ve sunucunun bu anahtarı daha önce işlediyse yeni kayıt oluşturmadan önceki sonucu döndürmesidir.
Örneğin istemci bir işlem kimliği üretebilir:
Sunucunun bu başlığı gerçekten desteklemesi ve anahtarı güvenilir biçimde saklaması gerekir. Sadece istemcinin başlık göndermesi tekrar kayıt oluşmasını engellemez. Sunucu, aynı anahtarla gelen sonraki isteği önceki işlemin sonucu olarak değerlendirmeli; aynı anahtarla farklı içerik gönderilirse bunu yeni işlem gibi kabul etmek yerine hata olarak ele almalıdır.
Aşağıdaki karşılaştırma hangi önlemin neyi çözdüğünü ayırır:
Bu önlemler birbirinin alternatifi değildir. Kullanıcı deneyimi için istemci tarafında kilit ve düğme durumu kullanılabilir; veri bütünlüğü için asıl güvence sunucuda ve veritabanında kurulmalıdır.
Önce tarayıcının gerçekten iki istek gönderip göndermediğini, yoksa tek isteğin sunucuda iki kez işlenip işlenmediğini ayırın. Geliştirici araçlarında ağ isteklerini açıp form gönderildiğinde aynı uç noktaya kaç
Tek bir
İki
submit olayını tek bir yerde yakalayın, ilk gönderimde bir kilit değişkeni etkinleştirin ve isteğin tamamlanmasına kadar gönderme düğmesini devre dışı bırakın. Ancak bu yalnızca tarayıcıdaki çift tıklamayı azaltır; ağ yeniden denemeleri, birden fazla sekme veya kötü niyetli istekler için sunucuda aynı işlemi ikinci kez uygulamayan bir yapı da kurulmalıdır.Formun tarayıcıda iki kez gönderilmesini engelleme
Aşağıdaki örnekte form gönderildiğinde tarayıcının normal sayfa yenilemesi
preventDefault() ile durdurulur. gonderiliyor değişkeni ilk gönderimden sonra true olur. Kullanıcı düğmeye tekrar bassa bile ikinci istek oluşturulmaz. Düğmenin devre dışı bırakılması ise kullanıcıya işlemin başladığını gösterir.Bu örneği bir HTML dosyasına koyup tarayıcıda açabilirsiniz. Gerçek uygulamada
'/api/siparis' adresini kendi sunucu uç noktanızla değiştirmelisiniz.
HTML:
<form id="siparisForm">
<label>
E-posta
<input id="eposta" name="eposta" type="email" required>
</label>
<button id="gonderButton" type="submit">Gönder</button>
<p id="mesaj" aria-live="polite"></p>
</form>
<script>
const form = document.querySelector('#siparisForm');
const gonderButton = document.querySelector('#gonderButton');
const mesaj = document.querySelector('#mesaj');
let gonderiliyor = false;
form.addEventListener('submit', async function (event) {
event.preventDefault();
if (gonderiliyor) {
return;
}
gonderiliyor = true;
gonderButton.disabled = true;
gonderButton.textContent = 'Gönderiliyor...';
mesaj.textContent = '';
try {
const formVerisi = new FormData(form);
const yanit = await fetch('/api/siparis', {
method: 'POST',
body: formVerisi
});
if (!yanit.ok) {
throw new Error('İstek başarısız oldu.');
}
mesaj.textContent = 'Form başarıyla gönderildi.';
form.reset();
} catch (hata) {
mesaj.textContent = 'Gönderim sırasında hata oluştu. Lütfen tekrar deneyin.';
// Hata durumunda kullanıcıya yeniden deneme imkânı verilir.
gonderiliyor = false;
gonderButton.disabled = false;
gonderButton.textContent = 'Gönder';
}
});
</script>
Buradaki akış şöyledir: Kullanıcı formu gönderir, olay işleyicisi çalışır, ilk satırlarda ikinci gönderim kontrol edilir ve ardından istek başlatılır. İstek başarılıysa düğme devre dışı kalmaya devam eder; form temizlenir ve başarı mesajı gösterilir. İstek hata verirse kilit kaldırılır. Böylece kullanıcı, başarısız işlemden sonra yeniden deneyebilir.
event.preventDefault() tek başına yeterli değildir. Bu çağrı, formun varsayılan gönderme davranışını durdurur; fakat aynı olay işleyicisi birden fazla kez çalıştırılmışsa veya kod içinde birden fazla istek başlatılmışsa bunları kendiliğinden engellemez. Bu nedenle gonderiliyor gibi bir kilit kontrolü gerekir.Sadece gönderme düğmesini devre dışı bırakmak yeterli mi?
Gönderme düğmesini devre dışı bırakmak, kullanıcının düğmeye art arda basmasını önler. Ancak bu kontrolü tek savunma olarak kullanmayın. Form, klavyeden Enter tuşuyla gönderilebilir; başka bir kod aynı form üzerinde
requestSubmit() çağırabilir veya olay işleyicisi yanlışlıkla iki kez tanımlanabilir. Kilit kontrolünü doğrudan submit olayının içinde yapmak bu durumların daha fazlasını kapsar.Birden fazla kod parçası aynı forma
submit dinleyicisi ekliyorsa her dinleyici kendi isteğini başlatabilir. Bu nedenle aynı form için gönderim mantığını tek bir yerde toplamak ve bileşen yeniden oluşturulduğunda dinleyicinin tekrar eklenmediğinden emin olmak gerekir. React, Vue veya başka bir arayüz kütüphanesi kullanıyorsanız olay işleyicisinin her çizimde yeniden kaydedilmemesine özellikle dikkat edin.Form doğrulamasını da gönderimden önce değil, gönderim akışının içinde düşünün.
required gibi yerleşik HTML doğrulaması başarısız olursa tarayıcı normalde submit olayını çalıştırmadan kullanıcıyı eksik alana yönlendirir. JavaScript ile özel doğrulama yapıyorsanız geçersiz veride kilidi etkinleştirmeden işlemi sonlandırın.Özel doğrulama kullanırken kilidi ne zaman etkinleştirmelisiniz?
Doğrulama başarısız olduğunda kullanıcı formu düzeltebilmelidir. Bu nedenle kilidi ancak veriler geçerli olduktan ve gerçekten ağ isteği başlatılmadan hemen önce etkinleştirin.
JavaScript:
form.addEventListener('submit', async function (event) {
event.preventDefault();
if (gonderiliyor) {
return;
}
const eposta = form.elements.eposta.value.trim();
if (!eposta.includes('@')) {
mesaj.textContent = 'Geçerli bir e-posta adresi girin.';
return;
}
gonderiliyor = true;
gonderButton.disabled = true;
// Buradan sonra ağ isteği başlatılır.
});
Bu sırada kullanıcı geçersiz e-posta girerse
gonderiliyor değeri false kalır ve düğme devre dışı bırakılmaz. Kullanıcı alanı düzelttikten sonra formu yeniden gönderebilir.İstek başarısız olduğunda kilit nasıl kaldırılır?
En sık yapılan hatalardan biri, düğmeyi devre dışı bıraktıktan sonra yalnızca başarılı yanıtı ele almaktır. Ağ bağlantısı koparsa veya sunucu hata döndürürse düğme kilitli kalır ve kullanıcı tekrar deneyemez. Bu nedenle hata durumunda kilidi kaldırın.
İstek tamamlandığında kilidi her durumda kaldırmak istiyorsanız
finally kullanabilirsiniz. Ancak başarı durumunda formun yeniden gönderilmesini istemiyorsanız, başarıdan sonra kilidi kaldırmamalısınız. Aşağıdaki yapı, başarısızlıkta yeniden denemeye izin verir; başarıda ise formu tamamlanmış kabul eder.
JavaScript:
form.addEventListener('submit', async function (event) {
event.preventDefault();
if (gonderiliyor) {
return;
}
gonderiliyor = true;
gonderButton.disabled = true;
gonderButton.textContent = 'Gönderiliyor...';
try {
const yanit = await fetch('/api/siparis', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
eposta: form.elements.eposta.value
})
});
if (!yanit.ok) {
throw new Error('Sunucu hatası');
}
mesaj.textContent = 'Kayıt oluşturuldu.';
} catch (hata) {
mesaj.textContent = 'İşlem tamamlanamadı. Yeniden deneyebilirsiniz.';
gonderiliyor = false;
gonderButton.disabled = false;
gonderButton.textContent = 'Gönder';
}
});
Başarı mesajının görünmesi, sunucunun işlemi kabul ettiğini gösterir; kullanıcının aynı sayfada başka bir form göndermesini veya işlemin arka planda kesin olarak tamamlandığını tek başına kanıtlamaz. Uygulamanızda sunucu yanıtının içeriğini de kontrol edin. Örneğin sunucu bir kayıt kimliği döndürüyorsa bu kimliği kullanıcıya gösterilen sonuçla ilişkilendirebilir veya durum sorgusu başlatabilirsiniz.
İstemci ve sunucu tarafındaki önlemler arasındaki fark
Tarayıcı tarafındaki kilit, aynı sayfada hızlı ve yanlışlıkla yapılan tekrarları önler. Fakat HTTP isteği sunucuya ulaştıktan sonra tarayıcı kontrolü artık tek başına yeterli değildir. Kullanıcı sayfayı yenileyebilir, iki sekme açabilir, bağlantı zaman aşımından sonra aynı isteği yeniden gönderebilir veya isteği doğrudan oluşturabilir.
Bu nedenle para çekme, sipariş oluşturma, üyelik kaydı veya e-posta gönderme gibi yan etkili işlemlerde sunucu aynı işlemi ikinci kez uygulamamalıdır. Bunun yaygın yolu, istemcinin her mantıksal işlem için benzersiz bir istek anahtarı göndermesi ve sunucunun bu anahtarı daha önce işlediyse yeni kayıt oluşturmadan önceki sonucu döndürmesidir.
Örneğin istemci bir işlem kimliği üretebilir:
JavaScript:
const istekAnahtari = crypto.randomUUID();
const yanit = await fetch('/api/siparis', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': istekAnahtari
},
body: JSON.stringify({
eposta: form.elements.eposta.value
})
});
Sunucunun bu başlığı gerçekten desteklemesi ve anahtarı güvenilir biçimde saklaması gerekir. Sadece istemcinin başlık göndermesi tekrar kayıt oluşmasını engellemez. Sunucu, aynı anahtarla gelen sonraki isteği önceki işlemin sonucu olarak değerlendirmeli; aynı anahtarla farklı içerik gönderilirse bunu yeni işlem gibi kabul etmek yerine hata olarak ele almalıdır.
Aşağıdaki karşılaştırma hangi önlemin neyi çözdüğünü ayırır:
| Önlem | Engellediği durum | Tek başına yeterli olmadığı durum |
|---|---|---|
gonderiliyor kilidi | Aynı sayfada art arda tetiklenen gönderimler | Yenileme, başka sekme veya sunucuya ulaşmış tekrar istek |
| Gönderme düğmesini devre dışı bırakma | Kullanıcının düğmeye tekrar basması | Klavye, başka kod veya doğrudan ağ isteği |
| Sunucu tarafında benzersiz işlem anahtarı | Aynı mantıksal isteğin yeniden gönderilmesi | Anahtar kontrolü olmayan veya hatalı tasarlanmış sunucu |
| Veritabanı benzersiz kısıtı | Aynı benzersiz değere sahip tekrar kayıtlar | İşlemin yan etkileri kayıt eklemekten daha genişse |
Form hâlâ iki kez gönderiliyorsa kontrol edilecek noktalar
Önce tarayıcının gerçekten iki istek gönderip göndermediğini, yoksa tek isteğin sunucuda iki kez işlenip işlenmediğini ayırın. Geliştirici araçlarında ağ isteklerini açıp form gönderildiğinde aynı uç noktaya kaç
POST isteği gittiğine bakın.Tek bir
POST isteği görünüyor fakat iki kayıt oluşuyorsa sorun büyük olasılıkla sunucu kodundadır. İstek işleyicisinin aynı veriyi iki kez kaydetmediğini, kuyruk tüketicisinin aynı mesajı tekrarlı işlemediğini ve veritabanında uygun benzersiz kısıtların bulunup bulunmadığını kontrol edin.İki
POST isteği görüyorsanız şu noktaları sırayla inceleyin:- Aynı forma birden fazla
submitdinleyicisi eklenip eklenmediğini kontrol edin.[/] - Hem
form.addEventListener('submit', ...)hem de HTML üzerindeonsubmitkullanılıp kullanılmadığını inceleyin.[/] - Gönderme düğmesinin
type="submit"olduğunu ve ayrıca ayrı birclickişleyicisinde ikinci birfetch()çağrılmadığını kontrol edin.[/] - Bileşen yeniden oluşturulurken dinleyicinin tekrar kaydedilmediğinden emin olun.[/]
- Form gönderiminde hem tarayıcının varsayılan davranışının hem de özel JavaScript isteğinin birlikte çalışmadığını kontrol edin.[/]
event.preventDefault() ile durdurun ya da özel istek kullanmayıp tek bir gönderim yöntemine geçin. Başarı ve hata mesajlarını da istek sayısından bağımsız olarak sunucunun gerçek yanıtına göre üretin.Sık sorulan sorular
preventDefault() neden tek başına çift gönderimi engellemiyor?
preventDefault() yalnızca tarayıcının form için varsayılan gönderme davranışını durdurur. Birden fazla olay dinleyicisi, art arda yapılan JavaScript çağrıları veya tekrar gönderilen ağ istekleri için ayrıca bir kilit, tekilleştirme anahtarı ya da sunucu tarafı kontrolü gerekir.Gönderme düğmesini ne zaman yeniden etkinleştirmeliyim?
İstek başarısız olduysa kullanıcı düzelterek tekrar deneyebilsin diye düğmeyi yeniden etkinleştirin. İstek başarılı olduysa aynı işlem için yeniden etkinleştirmeyin; yeni bir işlem başlatılacaksa formu temizleyip yeni gönderim durumunu açıkça başlatın.
Sunucu tarafında da önlem almak zorunda mıyım?
Yan etkili işlemlerde evet. Tarayıcıdaki kilit yalnızca o sayfadaki kullanıcı etkileşimini sınırlar; yenileme, başka sekme, ağ yeniden denemesi veya doğrudan gönderilen istekleri engellemez. Sunucu, benzersiz işlem anahtarı ve uygun veritabanı kısıtlarıyla aynı mantıksal işlemi ikinci kez uygulamamalıdır.