Если webhook-события пришли не по порядку, нельзя безусловно записывать в заказ статус из последнего полученного запроса. Безопасный обработчик сначала исключает повтор, затем сравнивает версию события с уже применённой и разрешает только допустимый переход состояния.
Порядок доставки не равен порядку бизнес-событий. Время получения запроса тоже не доказывает, какое изменение произошло раньше: ориентируйтесь на идентификатор события, версию объекта или sequence из контракта провайдера, а при их отсутствии сверяйте актуальное состояние через его API.
Почему webhook может опоздать или повториться
Отправитель может повторить запрос после тайм-аута, даже если ваш сервер уже выполнил операцию, но не успел вернуть ответ. События также могут обрабатываться параллельными очередями и идти разными маршрутами, поэтому более старое изменение иногда приходит после нового.
Типичные признаки проблемы:
- заказ уже оплачен, но снова стал «создан»;
- отменённая операция вернулась в промежуточный статус;
- одно действие дважды создало платёж, письмо или запись;
- в журнале время события старше, чем у ранее обработанной версии;
- два воркера почти одновременно изменили одну строку.
Если документация провайдера не обещает строгий порядок, проектируйте интеграцию так, будто его нет. Даже при таком обещании повторная доставка всё равно требует идемпотентности.
Как построить безопасный обработчик
1. Проверьте подлинность исходного запроса
Проверяйте подпись по необработанному телу запроса и правилам конкретного провайдера. Не меняйте JSON до вычисления подписи и не записывайте секрет подписи, токены или полный персональный payload в журналы.
Одновременно проверьте обязательные поля и допустимый возраст запроса, если это предусмотрено контрактом. Невалидное сообщение не должно попадать в обычную очередь обработки.
2. Сохраните событие до тяжёлой работы
Минимальная входящая запись обычно содержит провайдера, event_id, идентификатор заказа, тип события, версию или sequence, время события, время получения и хеш payload. На пару «провайдер + event_id» нужен уникальный индекс: повторный запрос тогда станет безопасным no-op, а не вторым действием.
Возвращайте успешный HTTP-ответ после того, как событие надёжно принято, а не после длинной цепочки писем, запросов и пересчётов. Тяжёлую работу выполняйте асинхронно; иначе тайм-аут увеличит число повторов.
3. Сравните версию, а не время получения
Храните у заказа последнюю применённую версию события. В транзакции заблокируйте запись заказа или используйте optimistic lock, затем сравните входящую версию с сохранённой:
- версия меньше или равна сохранённой — пометьте событие обработанным без изменения заказа;
- версия следующая и переход допустим — примените изменение;
- версия перескочила через ожидаемую — отложите событие и запустите сверку;
- версии нет — получите текущее состояние объекта через API провайдера и рассматривайте webhook как сигнал к сверке.
Поле created_at помогает расследованию, но само по себе слабее серверной версии: часы могут различаться, а несколько изменений — иметь одинаковую метку времени.
4. Ограничьте переходы конечным автоматом
Опишите разрешённые переходы явно. Например, для условной модели создан → оплачен → выполнен позднее событие «создан» не должно откатывать «оплачен». Переход «оплачен → возвращён» допустим только для соответствующего типа события и подтверждённой версии.
Не сравнивайте статусы по алфавиту и не присваивайте им случайные числовые «веса»: бизнес-процесс может иметь ветви отмены, возврата и ручной проверки. Таблица переходов должна отражать реальный жизненный цикл заказа.
5. Завершите изменение атомарно
Обновление заказа, отметка о применённом событии и запись следующего внутреннего действия должны быть одной транзакцией. Если письмо или запрос во внешнюю систему нельзя включить в транзакцию, сохраните его в outbox и отправьте отдельным воркером.
Так сбой между изменением статуса и отправкой уведомления не оставит систему в неопределённом состоянии. Для безопасных повторов исходящих запросов пригодится отдельная схема idempotency после тайм-аута.
Как найти уже случившийся сбой
Соберите цепочку по одному заказу и отсортируйте её двумя способами: по времени получения и по версии источника. Для каждой записи покажите event_id, тип, версию, предыдущий и новый статус, результат обработки и идентификатор воркера.
Проверяйте в таком порядке:
- был ли один
event_idпринят несколько раз; - пришла ли меньшая версия после большей;
- обработали ли два воркера одну версию параллельно;
- сохранился ли статус, но не сохранилась отметка обработки;
- был ли HTTP 2xx отправлен до надёжной записи события;
- доступен ли endpoint и не было ли тайм-аутов в момент повтора.
Не исправляйте историю массовым обновлением «последнего» статуса по времени получения. Сначала определите источник истины, выгрузите актуальные состояния у провайдера и подготовьте идемпотентный сценарий сверки с журналом изменений.
Что проверить перед выпуском
Автотесты должны воспроизводить не только обычную доставку, но и неприятные последовательности:
- одно событие приходит дважды;
- версии приходят в обратном порядке;
- два события обрабатываются параллельно;
- процесс падает после изменения заказа, но до ответа;
- пропущена промежуточная версия;
- подпись неверна или обязательное поле отсутствует;
- сверка через API временно недоступна.
После теста убедитесь, что итоговый статус один, побочное действие выполнено один раз, а повторный запуск не меняет результат. Контракт подписи, полей, кодов ответа и повторов сверяйте с документацией именно вашего провайдера.
Где заканчивается внешний мониторинг
Внешняя проверка помогает увидеть, что публичный endpoint недоступен, отвечает медленно или возвращает неожиданный HTTP-код. Но она не доказывает правильность подписи, порядок внутренних событий и переход статуса конкретного заказа.
В Web-Puls можно контролировать доступность публичного API-адреса и отделить сетевой сбой от ошибки бизнес-логики; ориентиры по выбору проверяемых адресов собраны в статье о мониторинге API endpoint.
Если неверные статусы уже влияют на реальные заказы, сохраните идентификаторы событий, версии, точное время с часовым поясом и обезличенный фрагмент журнала. Затем можно отправить заявку на профессиональную диагностику — без секретов, токенов и персональных данных.