Пошук уроків, статей та іншого контенту
Чому подвійний клік на кнопку «Оплатити» небезпечний без idempotency — і як ключі ідемпотентності це вирішують.
Операція ідемпотентна, якщо повторне її виконання з тими самими вхідними даними дає той самий результат, що й один виклик — незалежно від того, скільки разів її фактично викликали. Це не «операція нічого не робить при повторі», а «результат після N однакових викликів такий самий, як після одного».
GET — ідемпотентний за визначенням: багаторазове читання не змінює стан сервера.
PUT — ідемпотентний: PUT /users/42 з тим самим тілом запиту, викликаний двічі, лишає користувача 42 в однаковому кінцевому стані.
DELETE — ідемпотентний: перший виклик видаляє ресурс, повторні — застають ресурс уже видаленим (найчастіше повертають 404, але кінцевий стан системи не змінюється від додаткових викликів).
POST — НЕ ідемпотентний за замовчуванням: POST /orders, викликаний двічі через нестабільну мережу, у наївній реалізації створить два замовлення.
PATCH — залежить від реалізації: якщо тіло описує абсолютне нове значення поля, PATCH ідемпотентний; якщо тіло описує відносну зміну («збільшити лічильник на 1»), повторний виклик змінює результат — не ідемпотентний.
Класичний сценарій: користувач натискає «Оплатити», запит іде на сервер, але відповідь губиться через нестабільне мобільне з'єднання. Клієнт (або сам користувач, нетерпляче тиснучи кнопку вдруге) повторює той самий запит. Без жодного захисту сервер щиро вважає це двома окремими намірами оплатити — і списує гроші двічі.
Клієнт генерує унікальний ідентифікатор (зазвичай UUID) для конкретної спроби операції один раз і передає його в кожному повторному запиті через заголовок, наприклад Idempotency-Key:
POST /api/payments HTTP/1.1
Idempotency-Key: 8f14e45f-ceea-4c8e-8e9d-8b8f9b1f9f9e
Content-Type: application/json
{ "amount": 500, "currency": "UAH" }Сервер запам'ятовує, що конкретний Idempotency-Key уже був успішно оброблений (разом із результатом), і при повторному запиті з тим самим ключем просто повертає збережений результат замість того, щоб виконувати платіж повторно. Це переносить відповідальність за «однократність» операції з ненадійної мережі на явний, контрольований протокол — саме так працюють платіжні API на кшталт Stripe.
Ключ ідемпотентності має генерувати клієнт один раз на спробу дії користувача, а не сервер і не заново на кожен HTTP-запит — інакше повторна відправка того самого наміру просто отримає новий ключ і проблема подвійного списання нікуди не зникне.
Вважати, що додавання UUID у тіло запиту саме по собі «робить операцію ідемпотентною» — сервер повинен реально перевіряти цей ключ і не виконувати операцію повторно; без такої логіки на бекенді ключ — просто ще одне поле, яке ігнорується.
Зберігати результати idempotency-ключів без терміну придатності — реєстр використаних ключів зростатиме нескінченно; на практиці ключі зберігають з обмеженим TTL (наприклад, 24 години), достатнім, щоб покрити реалістичні повторні спроби через мережеві збої.
Помилково вважати GET-запити з побічними ефектами (аналітичний трекінг, лічильники переглядів) «безпечними, бо це GET» — HTTP-семантика ідемпотентності описує контракт API, а не гарантує, що реалізація йому справді відповідає.
Ідемпотентна операція дає однаковий кінцевий результат незалежно від кількості однакових повторних викликів. GET, PUT і DELETE ідемпотентні за дизайном HTTP, POST — ні, що робить операції зі побічними ефектами (платежі, створення замовлень) вразливими до подвійного виконання при повторних запитах через нестабільну мережу. Ключі ідемпотентності, які генерує клієнт і перевіряє сервер, — стандартний спосіб убезпечити такі операції без відмови від зручності автоматичних повторних спроб.