送信ボタンの連打や、戻る→再送信などで同じフォームが二重に送信されると、データの重複登録やサーバーの無駄な負荷につながります。これはJavaScriptとサーバー側の両面で防ぎます。
ポイントは多層で防ぐことです。①ボタン無効化(UI)、②フラグで送信中をガード(ロジック)、③サーバー側のワンタイムトークン(最後の砦)を組み合わせます。この記事ではそれぞれを動くコードで解説します。
disabled + isSubmitting フラグでガードします。ただしクライアントの対策は迂回できるため、確実な防止はサーバー側のワンタイムトークン(idempotencyキー)で行います。CSRFトークンは二重送信防止ではない点に注意してください。送信ボタンを無効化する(UI側の基本)
最も手軽なのは、送信時に送信ボタンを disabled にして連打を防ぐ方法です。インラインの onsubmit 属性ではなく、addEventListener でまとめます。
<form id="form" action="/submit"> <input type="text" name="name" required> <button type="submit">送信</button> </form>
const form = document.getElementById("form");
form.addEventListener("submit", () => {
const btn = form.querySelector('button[type="submit"]');
btn.disabled = true; // 送信後は押せなくなる(通常送信はそのまま進む)
});
<button name="action" value="save"> のようにボタンの値をサーバーで使っている場合は、disabledにすると値が届かなくなります。その場合は次のフラグ方式や、別の隠しフィールドで状態を送ってください。フラグで送信中をガードする(ロジック側)
ボタンの無効化だけだと、Enterキーやスクリプトからの送信をすり抜ける可能性があります。isSubmitting フラグで送信中かどうかを管理し、2回目以降を event.preventDefault() で止めると確実です。Enterキーによる送信そのものを制御したい場合はEnterキーでフォーム送信を無効化する方法も参考になります。
let isSubmitting = false;
form.addEventListener("submit", (event) => {
if (isSubmitting) {
event.preventDefault(); // 2回目以降の送信を止める
return;
}
isSubmitting = true;
});
通常のページ遷移を伴う送信ではページが切り替わるためフラグは自然にリセットされますが、次の非同期送信(fetch)では送信完了後にフラグを戻す処理が重要になります。
fetchでの非同期送信と二重送信防止
ページ遷移せずに送信する場合は fetch を使います。送信中はフラグとボタンでガードし、完了時に finally で必ず解除するのが定石です。古い XMLHttpRequest ではなく fetch + async/await で書きます。
const btn = form.querySelector('button[type="submit"]');
let isSubmitting = false;
form.addEventListener("submit", async (event) => {
event.preventDefault(); // 通常送信をキャンセル
if (isSubmitting) return; // 送信中なら無視
isSubmitting = true;
btn.disabled = true;
try {
const res = await fetch(form.action, {
method: "POST",
body: new FormData(form),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
// 成功時の処理(例:完了画面へ遷移など)
} catch (err) {
console.error("送信に失敗しました", err);
} finally {
isSubmitting = false; // 成否に関わらず必ず解除
btn.disabled = false;
}
});
finally で解除することで、エラー時に送信できなくなる事故を防げます(成功時はそのまま完了画面へ遷移し、再有効化しない設計も有効です)。fetch の基本はfetch APIの使い方、フォームデータの送信はFormDataの使い方、送信中のローディング表示はフォーム送信のローディング実装を参考にしてください。
サーバー側が本命:ワンタイムトークン(idempotency)
クライアント側の対策は開発者ツールなどで簡単に迂回できます。ネットワークの遅延でリクエストが二重に届くこともあります。そのため確実な二重送信防止はサーバー側で行います。
定番はワンタイムトークンです。フォーム表示時に一意のトークンを発行してサーバーに保存し、送信を受け取ったらそのトークンを使い捨て(消費)します。同じトークンで2回目が来たら「処理済み」として弾く仕組みです。APIならidempotencyキー、画面遷移ならPRG(Post/Redirect/Get)パターンと組み合わせます。
CSRFトークンはなりすまし(別サイトからの不正送信)対策で、通常はセッション中何度でも再利用できるため、それ自体では二重送信を防げません。二重送信を防ぐには、一度使ったら無効になるワンタイムトークンが必要です。両者は目的が異なるので混同しないようにしましょう。よくある質問(FAQ)
submit イベントで送信ボタンを disabled にするのが基本です。さらに isSubmitting のようなフラグ変数で送信中を管理し、2回目以降を event.preventDefault() でキャンセルすると確実です。isSubmitting フラグを boolean で管理し、送信中は true にして再送信を防ぎ、完了後に finally で false に戻します。ボタンの disabled も併用すると、UIとロジックの両面で防げます。まとめ
フォームの二重送信は、①ボタンの disabled(UI)→ ②isSubmitting フラグ+preventDefault(ロジック)→ ③サーバー側のワンタイムトークン(最後の砦)という多層で防ぐのが基本です。
非同期送信では fetch + try/finally で成否に関わらず確実に状態を戻すのがポイントです。そしてCSRFトークンは二重送信防止ではないことを押さえ、確実な防止はサーバー側のワンタイムトークン(idempotencyキー)で行いましょう。
