intDataBlog
← ЖурналМаршруты обучения ↗

Идемпотентность: повторить запрос и не повторить действие

Ключ операции, конкурирующие запросы, изменившиеся параметры и границы защиты от дублей.

Клиент нажал кнопку, связь оборвалась, ответ не пришёл. Он нажимает ещё раз. Для интерфейса это два запроса, но для человека — одно действие. Сервер должен различать попытку доставки и саму операцию.

Для HTTP идемпотентность означает одинаковый предполагаемый эффект одного и нескольких одинаковых запросов. Ответы при этом могут отличаться. Например, повтор удаления не обязан вернуть тот же статус, что первый вызов. RFC 9110

Ключ живёт дольше сетевой попытки

Возьмём учебную операцию создания заявки на доставку. Клиент генерирует ключ, сохраняет его вместе с намерением и использует при повторе. Если создавать новый ключ для каждого нажатия или таймаута, сервер увидит разные операции и не сможет объединить их.

Ключ нужно связать с контуром пользователя и типом действия. Иначе одинаковая строка в двух аккаунтах способна столкнуться в общей таблице. Внешний ключ клиента не заменяет авторизацию: перед возвратом ранее сохранённого результата сервер заново проверяет, кому он принадлежит.

Практический пример такого API — поддержка Idempotency-Key в Stripe. Это пример конкретного контракта провайдера, а не общий стандарт поведения любого POST. Правила сохранения и повторного использования ключа нужно читать у каждого сервиса отдельно. Stripe: idempotent requests

Одной проверки «уже есть?» недостаточно

Два обработчика могут одновременно проверить отсутствие ключа и оба перейти к созданию заявки. Защищать границу нужно атомарно: например, уникальным ограничением на пару «контур + ключ» и транзакцией, которая связывает регистрацию операции с локальным эффектом.

Учебная таблица могла бы выглядеть так:

CREATE TABLE operation_keys (
  account_id text NOT NULL,
  operation_key text NOT NULL,
  request_fingerprint text NOT NULL,
  state text NOT NULL,
  response_body jsonb,
  PRIMARY KEY (account_id, operation_key)
);

Это только каркас. Чтобы он стал рабочим механизмом, нужен протокол переходов: кто владеет выполняющейся операцией, что получает второй запрос, как распознать зависшую обработку и когда разрешить восстановление. Нельзя просто заменить состояние на «готово», если первый исполнитель ещё способен продолжить работу.

Отпечаток существенных параметров помогает заметить другой конфликт: тот же ключ прислали для другой заявки. Такой случай лучше отклонить явно, а не вернуть результат первой операции для новых данных. Способ канонизации запроса является частью контракта; хеш произвольной JSON-строки может различаться из-за порядка полей.

Webhook: доставка и бизнес-событие

Во входящем webhook ключ обычно приходит от источника. Stripe предупреждает о повторной доставке событий и предлагает учитывать их ID; порядок доставки также не гарантируется. Stripe: receive events

Для учебной интеграции сначала проверяют подлинность сообщения, затем надёжно сохраняют его, и только после этого подтверждают приём согласно правилам источника. Если ответить успехом раньше сохранения, падение процесса может оставить необработанное событие без повода для повторной доставки.

Дедупликация по ID защищает от повторов одного сообщения. Два разных события об одном бизнес-действии требуют другого правила: например, перехода состояния заявки, который разрешён только один раз. Иначе транспортная защита работает, а дубли в предметной области остаются.

Минимальный набор проверок

Одну операцию отправляют последовательно и одновременно, с тем же ключом и изменёнными параметрами. Затем имитируют падение после эффекта, но до ответа. Проверяют итоговое число заявок, принадлежность результата аккаунту и восстановление зависшей операции.

Наконец, задают срок хранения ключей. После его истечения прежний запрос может снова выглядеть новым. Это ограничение должно совпадать с реальными окнами повторов и бизнес-правилами, а не быть случайным значением TTL.