Идемпотентность
Идемпотентность нужна с обеих сторон, и это две разные задачи. На отправке — чтобы повтор запроса не создал второй заказ. На приёме — чтобы повтор вебхука не выдал доступ дважды.
Обе описаны здесь, потому что забывают обычно вторую.
Зачем это на отправке #
Сеть теряет ответы, а не запросы. Клиент отправил создание платежа, получил таймаут — и не знает, создался платёж или нет. Оба варианта плохи: повторить — риск списать дважды, не повторить — риск не списать вовсе.
Ключ идемпотентности снимает выбор: повторять можно всегда.
Как пользоваться #
curl -X POST 'https://school.example.com/api/v1/orders' \
-H 'Authorization: Bearer lms_live_a8f3c1e9d2b74a5f8e0c1d3b5a7f9e2c' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 6f1e2a34-9c8b-4d7e-a1f0-3b5c7d9e1f2a' \
-H 'Accept: application/json' \
--data '{ "user_id": "usr_01HQZX3M8K4N2P", "offer_id": "off_01HQZX7QGVPR6A" }'
Ключ придумывает клиент. Требование одно: он должен быть уникален для операции, а не для попытки. Все повторы одной и той же операции идут с одним ключом — в этом весь смысл.
Повтор с тем же ключом возвращает тот же результат, что и первый запрос, и добавляет заголовок:
Idempotency-Replayed: true
Не создаёт второй заказ, не списывает деньги дважды. Ключ хранится 24 часа.
Откуда брать ключ #
Хороший ключ — тот, который вы можете воспроизвести после перезапуска своего
процесса. Случайный uuid, сгенерированный в момент отправки и не сохранённый,
защищает только от повтора внутри одной попытки — то есть от самого редкого случая.
Практически: генерируйте ключ вместе с задачей и храните его рядом с ней. Если задача перезапустилась, она берёт тот же ключ.
Тот же ключ с другим телом #
Это ошибка клиента, и отвечаем мы 409 с пояснением.
Молча создать новый объект нельзя: почти наверняка у клиента баг генерации ключей — например, ключ считается от времени с точностью до секунды, — и лучше он узнает об этом сразу, чем через месяц по расхождению в отчётах.
Где ключ обязателен #
Там, где повтор опасен: создание заказа, платёж, возврат, выдача доступа, отправка сообщения.
Оговорка честности: в спецификации это сказано прозой, а перечня операций нет.
Поэтому в машинной спецификации Idempotency-Key помечен необязательным везде —
пометить его обязательным наугад значило бы соврать про контракт. Признак придёт
из кода вместе с эндпоинтом.
Практический совет до этого момента: посылайте ключ на всех операциях записи. Лишним он не будет никогда.
Идемпотентность на приёме #
Вторая половина, и её забывают чаще.
Вебхук может прийти дважды: при повторе после таймаута, при переигрывании
пропущенного, при обрыве после того, как вы уже всё обработали. Поэтому
обработчик обязан быть идемпотентным по event.id:
1. Взять event.id из конверта.
2. Вставить его в свою таблицу обработанных — с УНИКАЛЬНЫМ индексом.
3. Вставка не прошла: событие уже обработано. Ответить 200 и выйти.
4. Вставка прошла: обработать.
Именно уникальный индекс, а не проверка «есть ли такая запись» перед вставкой: два экземпляра обработчика пройдут проверку одновременно и оба обработают. Гонка редкая — и потому находится в самый неудобный момент.
То же самое делаем и мы у себя на приёме уведомлений от платёжных провайдеров: уникальный индекс по паре «провайдер, внешний идентификатор». Повтор уведомления не создаёт второй платёж и не выдаёт доступ дважды.
Чего идемпотентность не делает #
Не делает операцию обратимой. Возврат платежа идемпотентен по ключу, но необратим по сути: повтор с тем же ключом вернёт тот же результат, а новый ключ сделает второй возврат.
Не заменяет проверку состояния. Отмена уже отменённого заказа даст 409 —
это конфликт состояния, а не задача для ключа.