ТестированиеТестирование API и интеграцийИнженер по интеграционному тестированию

Сервис повторяет HTTP запрос после тайм аута. Какой контракт должен защитить операцию от двойного эффекта?

Сервис повторяет HTTP-запрос после тайм-аута. Какой контракт должен защитить операцию от двойного эффекта?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

Сетевой тайм-аут не показывает, выполнилась операция или нет: ответ мог потеряться уже после успешной записи на сервере. Поэтому распределённые клиенты используют повторные попытки, особенно для временных ошибок и разрывов соединения.

Без специального контракта повтор безопасен только для операций, которые сами по себе не меняют состояние повторно. Для создания платежа, заказа или резервирования этого обычно недостаточно: повтор может породить второй объект или повторно списать средства.

Постановка проблемы

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

Если сервер обрабатывает повторы как новые независимые запросы, появляются дубли, двойные списания, повторные уведомления или рассинхронизация между системами. Проверка только HTTP-статуса не выявляет проблему: оба запроса могут вернуть успешный статус, хотя бизнес-эффект возник дважды.

Подробное решение

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

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

Минимальный пример контракта:

POST /payments Idempotency-Key: payment-7f3a Content-Type: application/json {"amount":1000,"currency":"RUB"}

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

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

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

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

Ситуация из практики

Платёжный сервис иногда возвращал тайм-аут после передачи запроса в банковский шлюз. Рассматривались два варианта: увеличить тайм-аут и отключить повторы либо добавить ключ идемпотентности на границе платёжного API.

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

Выбрали идемпотентный контракт. Сервис сохранял ключ, нормализованный набор параметров и итог операции, а интеграционный тест имитировал потерю ответа после обработки первого запроса и затем отправлял повтор.

Тест проверял, что в платёжной системе существует ровно одна операция, повтор получает тот же идентификатор результата, а запрос с тем же ключом и другой суммой отклоняется. Это защищало не только HTTP-слой, но и фактический бизнес-эффект.

Что кандидаты часто упускают

  1. Достаточно ли проверить, что повтор возвращает тот же HTTP-статус?

Нет. Одинаковый статус не доказывает отсутствие двойного эффекта: два независимых запроса могут оба вернуть успешный ответ. Проверка должна наблюдать внешний результат — количество созданных объектов, число списаний, состояние резервирования или опубликованные события.

  1. Что должен сделать сервер при одновременных запросах с одним ключом?

Он не должен позволить двум обработчикам одновременно пройти проверку отсутствия ключа и создать две операции. Регистрация ключа и переход к обработке должны быть защищены атомарностью, уникальным ограничением или эквивалентным механизмом; иначе последовательный тест повторов может проходить, а гонка в производственной системе — нет.

  1. Можно ли считать операцию идемпотентной, если основная запись защищена, но событие отправляется повторно?

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