HTTP-сервис 1С: принять заказ ровно один раз

Курьер нажал «Оплатить», сайт отправил заказ, но не дождался ответа 1С и повторил запрос. Если обработчик сразу создаёт документ, в базе появятся два заказа.

Фраза «доставить ровно один раз» звучит как настройка транспорта, но в распределённой системе её обеспечивает прикладной код. Сеть умеет потерять ответ после успешной записи, балансировщик — повторить запрос, а человек — нажать кнопку ещё раз. Поэтому входящий сервис должен распознавать уже обработанное событие и возвращать прежний результат.

Что делает обработчик, а чего не делает

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

Для нашего магазина определим POST /integration/v1/orders/paid. Отправитель обязан передать X-Correlation-ID; если его нет, сервис создаёт новый для диагностики. Ответ всегда содержит этот идентификатор.

Регистр принятых событий

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

  1. Проверить синтаксис и обязательные поля без транзакции.
  2. Начать короткую транзакцию и заблокировать запись по EventID.
  3. Если событие завершено и хеш совпадает — вернуть сохранённый ответ.
  4. Если идентификатор тот же, а хеш другой — ответить 409 Conflict.
  5. Создать заказ и зафиксировать результат одной транзакцией.
Функция ОбработатьОплату(Событие, ХешТела) Экспорт
    НачатьТранзакцию();
    Попытка
        Блокировка = Новый БлокировкаДанных;
        Элемент = Блокировка.Добавить("РегистрСведений.ПринятыеСобытия");
        Элемент.УстановитьЗначение("EventID", Событие.event_id);
        Блокировка.Заблокировать();

        Прежний = НайтиПринятоеСобытие(Событие.event_id);
        Если Прежний <> Неопределено Тогда
            Если Прежний.ХешТела <> ХешТела Тогда
                ВызватьИсключение "EVENT_ID_REUSED";
            КонецЕсли;
            ЗафиксироватьТранзакцию();
            Возврат Прежний.Результат;
        КонецЕсли;

        Заказ = СоздатьЗаказПоСобытию(Событие);
        СохранитьРезультатСобытия(Событие.event_id, ХешТела, Заказ.Ссылка);
        ЗафиксироватьТранзакцию();
        Возврат Заказ.Ссылка;
    Исключение
        ОтменитьТранзакцию();
        ВызватьИсключение;
    КонецПопытки;
КонецФункции

Коды ответа — обещание отправителю

201заказ создан впервыеповторять не нужно
200повтор события, возвращён прежний результатсчитать доставленным
400JSON не разобранисправить формат
409event_id повторно использован с другим теломразобрать конфликт
422формат верен, бизнес-данные неприемлемыисправить данные
503временная зависимость недоступнаповторить позже тем же ключом

Не возвращайте 200 вместе с текстом ошибки. Многие клиенты смотрят только на класс статуса и навсегда пометят событие доставленным. По той же причине не стоит отвечать 500 на ожидаемую ошибку в данных: отправитель будет бессмысленно повторять её.

Быстрый ответ или синхронная запись?

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

Аварийный тест: после записи заказа искусственно оборвите соединение до отправки HTTP-ответа. Повтор тем же event_id обязан вернуть ссылку на первый документ, а не создать новый.

Проверьте себя
1. Почему одного уникального номера заказа недостаточно для идемпотентности событий?
AНомер нельзя хранить в 1С
BОдин заказ может породить разные события
CHTTP не передаёт строки
DУ номера всегда меняется длина
2. Что должен вернуть сервис при повторе завершённого события с тем же телом?
AСоздать новый документ и вернуть 201
BВернуть сохранённый результат без нового документа
CУдалить предыдущий документ
DВсегда вернуть 409
3. Когда уместен HTTP 202 Accepted?
AКогда сообщение принято в очередь, но результат ещё не готов
BПосле любой ошибки JSON
CКогда заказ создан дважды
DТолько для GET-запроса