Idempotensi dalam API — Mengapa POST Anda Dijalankan Dua Kali dan Bagaimana Memperbaikinya
Pembayaran yang dibayar dua kali, pesanan yang duplikat, atau pengguna yang dibuat tiga kali — ini adalah gejala dari kontrak API yang hilang. Berikut arti idempotency, mengapa POST menghancurkannya, dan bagaimana kunci idempotency memperbaikinya.
Pengguna Anda mengklik Bayar Sekarang. Spinner berputar. Waktu jaringan habis. Logika retry Anda aktif. Pembayaran berhasil dilakukan. Kemudian permintaan awal juga selesai di sisi server — dan pembayaran dilakukan lagi. $200 ditarik dari akun pelanggan, dan tiket dukungan menunggu Anda di pagi hari.
Ini bukan kasus ekstrem yang langka. Ini terjadi ketika Anda tidak mempertimbangkan idempotensi sebelum Anda membutuhkannya. Mari kita perbaiki hal ini.
Apa arti idempotensi sebenarnya
Kata ini berasal dari matematika. Operasi dikatakan idempoten jika diterapkan lebih dari sekali menghasilkan hasil yang sama seperti ketika diterapkan sekali. f(f(x)) = f(x).
Dalam konteks API: memanggil endpoint yang sama dengan niat yang sama sebanyak N kali harus menghasilkan keadaan sistem yang sama seperti ketika hanya dipanggil sekali. Respons bisa berupa hasil yang disimpan, tetapi efek samping — seperti penulisan ke database, pembayaran, atau pengiriman email — hanya terjadi sekali.
Ini adalah jaminan tentang apa yang dilakukan oleh server tidak, bukan hanya apa yang dikembalikan.
Metode HTTP: siapa yang aman dan siapa yang tidak
Spesifikasi HTTP menetapkan beberapa metode sebagai idempoten secara definisi. Berikut adalah penjabaran praktisnya:
| Metode | Idempoten? | Mengapa |
|---|---|---|
GET | ✅ Ya | Baca saja. Tidak ada efek samping secara definisi. |
HEAD | ✅ Ya | Sama seperti GET, tidak mengembalikan badan. |
PUT | ✅ Ya | "Atur sumber daya ini ke keadaan tepat seperti ini." Memanggil dua kali menghasilkan hasil yang sama. |
DELETE | ✅ Ya | Sumber daya hilang setelah panggilan pertama. Panggilan berikutnya menemukan tidak ada yang dihapus. (Kode status bisa berbeda — 204 vs 404 — tetapi keadaan server tidak berubah.) |
POST | ❌ Tidak | "Proses payload ini." Artinya tergantung pada server. Dua POST biasanya menciptakan dua sumber daya atau memicu dua efek samping. |
PATCH | ⚠️ Tergantung | Perubahan relatif ("increment count by 1") tidak idempoten. Perubahan absolut ("set count to 5") adalah. Implementasi Anda menentukan mana yang mana. |
Masalahnya adalah bahwa kebanyakan operasi bisnis nyata — membayar pembayaran, membuat pesanan, mengirim notifikasi — secara alami dipetakan ke POST. Dan POST memberikan jaminan idempotensi nol secara bawaan.
Urutan yang menghasilkan
Berikut adalah mode kegagalan klasik, langkah demi langkah:
Client Network Server
|
|--- POST /payments ------>| |
| |--- (delivered) -------->|
| | processing...
| | card charged ✓
|<-- (connection drops) ---| response queued
|
| [retry logic kicks in]
|
|--- POST /payments ------>| |
| |--- (delivered) -------->|
| | processing...
| | card charged ✓ (again)
|<------- 200 OK ----------|<------------------------|
|
[client sees: one charge. server did: two charges]
Klien tidak pernah melihat respons pertama. Dari perspektif klien, permintaan gagal. Dari perspektif server, permintaan berhasil dua kali. Ini adalah kasus klasik sistem terdistribusi — klien dan server memiliki perbedaan tentang apa yang terjadi.
Ini bukan hanya prosesor pembayaran. Ini juga berlaku untuk pembuatan pesanan, pendaftaran pengguna, pengiriman email, reservasi stok — apa pun yang memiliki konsekuensi nyata jika "dipicu dua kali".
Kunci idempotensi: pola Stripe
Stripe mempopulerkan pendekatan yang sekarang digunakan oleh kebanyakan API pembayaran. Klien menghasilkan kunci unik sebelum mengirim permintaan dan menempatkan kunci tersebut sebagai header. Server menggunakan kunci tersebut untuk menghindari duplikasi.
Sisi klien:
// Generate once, before any retry loop
const idempotencyKey = crypto.randomUUID();
async function chargeWithRetry(amount, retries = 3) {
for (let attempt = 0; attempt < retries; attempt++) {
try {
const response = await fetch('/api/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey, // same key on every retry
},
body: JSON.stringify({ amount, currency: 'usd' }),
});
if (response.ok) return await response.json();
// Don't retry on client errors (4xx)
if (response.status < 500) throw new Error(`Client error: ${response.status}`);
} catch (err) {
if (attempt === retries - 1) throw err;
await sleep(Math.pow(2, attempt) * 1000); // exponential backoff
}
}
}
Kunci harus dihasilkan sebelum dalam setiap loop retry — itu adalah inti dari mekanisme ini. Jika Anda menghasilkan UUID baru setiap kali, Anda telah menghancurkan mekanisme tersebut sepenuhnya.
UUID v4 bekerja dengan baik di sini. Jika Anda perlu menghasilkannya cepat saat menguji, IO Tools' generator UUID memungkinkan Anda menghasilkan sejumlah besar tanpa mengunduh library.
Sisi server: simpan respons, kembalikan saat ulang
Tugas server secara konseptual sederhana: periksa apakah kunci ini sudah pernah dilihat sebelumnya; jika ya, kembalikan respons yang disimpan; jika tidak, proses dan simpan.
// Express + Redis
app.post('/api/payments', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (idempotencyKey) {
const cached = await redis.get(`idem:${idempotencyKey}`);
if (cached) {
const { status, body } = JSON.parse(cached);
return res.status(status).json(body);
}
}
const result = await paymentProcessor.charge(req.body);
const responseBody = { id: result.id, status: result.status };
if (idempotencyKey) {
// 24-hour TTL — same window Stripe uses
await redis.setex(
`idem:${idempotencyKey}`,
86400,
JSON.stringify({ status: 200, body: responseBody })
);
}
res.json(responseBody);
});
Beberapa detail yang penting:
- Kegagalan cache, bukan hanya keberhasilan. Jika pembayaran gagal (kartu ditolak), simpan juga respons kegagalan tersebut. Jika tidak, retry akan mencoba pembayaran lagi meskipun sebelumnya sudah mengembalikan kesalahan — yang tepat Anda coba hindari.
- Validasi konsistensi badan. Stripe mengembalikan 422 jika kunci yang sama digunakan dengan parameter permintaan yang berbeda. Ini menangkap bug di mana Anda secara tidak sengaja menggunakan kunci yang sama di operasi yang berbeda.
- Tangani duplikasi yang sedang berlangsung. Dua permintaan dengan kunci yang sama yang tiba secara bersamaan membutuhkan koordinasi — proses satu dan kembalikan 409 pada yang lain, atau gunakan lock terdistribusi. Redis SETNX adalah pola umum di sini.
- Tetapkan TTL yang wajar. Setelah kedaluwarsa, kunci dapat dikembalikan dan permintaan dengan kunci yang sama dianggap sebagai permintaan baru. Jangan simpan selamanya — cache Anda akan membesar.
Kesalahan klien yang memicu POST dua kali tanpa retry
Kunci idempotensi melindungi terhadap retry pada lapisan jaringan. Mereka tidak membantu ketika klien memicu dua permintaan terpisah dan independen. Penyebab umumnya:
- Klik ganda pada tombol submit. Pengguna mengklik, tidak ada yang terjadi, lalu mengklik lagi. Nonaktifkan tombol segera saat klik pertama — bukan setelah respons tiba. Celah antara klik dan respons adalah saat klik kedua terjadi.
- Pemanggilan dua kali dalam React StrictMode. React 18 Strict Mode menjalankan efek dua kali dalam pengembangan untuk mengungkapkan bug. Jika Anda memicu POST dalam sebuah
useEffecttanpa pembersihan, Anda akan melihat permintaan ganda dalam pengembangan. Ini tidak terjadi di produksi, tetapi dapat menyembunyikan masalah nyata. - Pengiriman ulang form di browser. Mengirim → berpindah → kembali → maju. Beberapa browser meminta "mengirim ulang?", beberapa hanya melakukannya. Pola POST-Redirect-GET (mengembalikan redirect setelah POST) menghilangkan hal ini secara utuh.
- Kondisi persaingan dalam tangan pengguna. Klik dan tekanan Enter cepat secara bersamaan memicu tangan pengguna sebelum respons pertama tiba dan menonaktifkan form.
- Pengulangan latar belakang aplikasi mobile. Logika pengambilan latar belakang iOS dan Android atau retry jaringan dapat mengulang permintaan yang sudah dikirim oleh aplikasi. Hasilkan kunci idempotensi saat keinginan pengguna, simpan secara lokal jika diperlukan, dan hapus hanya setelah keberhasilan dikonfirmasi.
Alternatif yang perlu dipertimbangkan: PUT ke URL UUID
Jika Anda mengontrol kedua ujung API, ada opsi yang lebih bersih secara struktur untuk pembuatan sumber daya: biarkan klien menentukan ID sumber daya dan gunakan PUT alih-alih POST.
# Instead of this (POST, not idempotent):
POST /orders
{"amount": 99.00, "items": [...]}
# Do this (PUT to client-generated UUID, idempotent by spec):
PUT /orders/7f3b9c2a-4e5d-4f8b-9a1c-2d3e4f5a6b7c
{"amount": 99.00, "items": [...]}
PUT adalah idempoten secara spesifikasi HTTP — "atur sumber daya ini ke keadaan ini" berarti mengulang PUT tidak memiliki efek tambahan setelah sumber daya sudah ada. Server menangani duplikasi dengan INSERT ... ON CONFLICT DO NOTHING atau setara.
Polanya bekerja dengan baik untuk pembuatan pesanan, manajemen draft, dan setiap sumber daya di mana konsistensi ID selama retry penting. Pola ini tidak bekerja ketika pihak ketiga (sebuah prosesor pembayaran) menentukan ID, atau ketika efek samping harus terjadi di sisi server sebelum Anda tahu ID.
Implementasi sebenarnya Stripe
Dokumentasi kunci idempotensi Stripe sangat worth dibaca secara lengkap. Beberapa detail yang penting dalam praktiknya: Format kunci:
- Sebarang string hingga 255 karakter, terbatas pada akun Stripe Anda. Dua akun berbeda yang menggunakan string yang sama tidak saling mengganggu. Jendela 24 jam:
- Kunci habis setelah 24 jam. Retry setelah kedaluwarsa dianggap sebagai permintaan baru — penting untuk diketahui jika Anda membangun alur kerja yang panjang. Kesalahan body = 422:
- Kunci yang sama, parameter berbeda → Stripe mengembalikan 422 Unprocessable Entity. Ini adalah perilaku yang benar; ia menangkap bug di mana Anda secara tidak sengaja menggunakan kunci yang sama. Pemrosesan secara bersamaan:
- Jika dua permintaan dengan kunci yang sama tiba secara bersamaan, Stripe memproses satu dan mengembalikan 409 Konflik pada yang lain. Ulangi permintaan 409 setelah jeda singkat. Kegagalan yang disimpan di cache:
- Jika pembayaran gagal (kartu ditolak), Stripe menyimpan kegagalan tersebut. Mencoba dengan kunci yang sama mengembalikan penolakan yang sama. Anda perlu kunci baru untuk mencoba kartu yang berbeda — yang benar, karena upaya sebelumnya adalah operasi yang penuh dan sengaja. Menguji idempotensi tanpa suite pengujian integrasi penuh
Cara tercepat untuk memverifikasi perilaku adalah dengan mengirimkan endpoint dua kali dengan kunci yang sama dan memeriksa bahwa permintaan kedua mengembalikan respons yang disimpan tanpa memicu efek samping lagi.
IO Tools' pembangun perintah cURL membuatnya mudah untuk membangun permintaan dengan header khusus — termasuk — tanpa menghafal sintaks perintah curl. Idempotency-Key Skenario yang perlu ditangani:
Kunci yang sama + badan yang sama, dalam TTL → respons yang disimpan dikembalikan, efek samping terjadi sekali
- Kunci yang sama + badan yang berbeda → 422 (atau status konflik yang Anda pilih)
- Kunci yang sama setelah TTL habis → dianggap sebagai permintaan baru
- Tidak ada kunci yang disediakan → diproses secara normal (tentukan secara awal apakah kunci diperlukan atau opsional)
- Dua permintaan secara bersamaan dengan kunci yang sama → satu diproses, satu mendapatkan 409
- POST tidak idempoten, dan celah antara "permintaan dikirim" dan "respons diterima" adalah tempat di mana efek samping ganda hidup. Solusi ini tidak rumit: hasilkan UUID sebelum setiap loop retry, kirimkan sebagai header, simpan respons di sisi server dengan kunci header tersebut. Bagian yang rumit adalah detail-detailnya — penyimpanan kegagalan, penanganan duplikasi bersamaan, memilih TTL yang tepat — tetapi pola inti cukup sederhana untuk ditambahkan ke setiap endpoint yang penting.
AES-256-GCM
Tambahkan dukungan kunci idempotensi sebelum lalu lintas produksi pertama kali mencapai. Memperbaikinya setelah insiden pembayaran ganda adalah waktu yang jauh lebih buruk untuk belajar pelajaran ini.
Idempotensi dalam API — Mengapa POST Anda Dikirim Dua Kali dan Bagaimana Memperbaikinya 2
Instal Ekstensi Kami
Tambahkan alat IO ke browser favorit Anda untuk akses instan dan pencarian lebih cepat
恵 Papan Skor Telah Tiba!
Papan Skor adalah cara yang menyenangkan untuk melacak permainan Anda, semua data disimpan di browser Anda. Lebih banyak fitur akan segera hadir!
Alat Wajib Coba
Lihat semua Pendatang baru
Lihat semuaMemperbarui: Kita alat terbaru ditambahkan pada 15 Juni 2026
