Контракт заявки и входящий webhook
В прошлом уроке мы описали процесс обработки предложения статьи. Теперь нужно договориться, какие данные считаются заявкой. Без этого одинаковые по смыслу сообщения будут поступать в разных формах: одна система передаст title, другая — name, третья вложит заголовок в объект article. Результатом урока станет небольшой контракт входа и схема адаптации webhook к нашему примеру.
Продолжаем editorial-intake-lab с заявками P-001, P-002 и P-003. Все запросы ниже описаны как ожидаемые; адреса вымышленные, слушатель не запускался. Мы не подключаем форму ProfessorWeb и не отправляем данные посетителей. HTTP нужен здесь для понимания границы между транспортом и редакционным содержимым.
Собственные файлы этого шага доступны в архиве fixtures и контрактов серии, папка lesson-02. Это приложения к объяснению, а не готовый экспорт workflow.
Заявка внутри HTTP-события
API задаёт способ взаимодействия программ: доступные операции, формат данных и ожидаемые ответы. Webhook использует этот способ для передачи события получателю. Вместо периодического вопроса «появилось ли новое предложение?» отправитель сообщает о конкретной заявке. Получатель всё равно должен проверить форму сообщения; сам факт HTTP-вызова не делает содержание достоверным.
Выберем событие proposal.created и версию договора 1. В теле запроса располагается объект proposal с идентификатором, заголовком, темой и кратким описанием. Имя события позволяет позднее отличить создание предложения от его обновления. Версия фиксирует ожидания полей: изменение структуры не должно происходить незаметно для получателя.
{
"schema_version": 1,
"event": "proposal.created",
"proposal": {
"id": "P-001",
"title": " Проверка старых URL ",
"topic": " WEB ",
"summary": "Разобрать реестр прежних адресов и ожидаемые ответы.",
"approved": false
}
}
Это тело предполагаемого запроса, а не полный объект входа узла n8n. Webhook может представить его рядом с заголовками, параметрами адреса и query-параметрами. Поэтому путь к заголовку в таком входе отличается от пути в ручном образце. Дальше понадобится адаптер, который выбирает редакционный объект и не тащит транспортные сведения через весь workflow.
| Поле | Договорённость проекта | Неверный вариант | Ожидаемый исход |
|---|---|---|---|
schema_version |
Число 1 |
Строка "1" |
Несовместимый вход |
event |
proposal.created |
Неизвестное событие | Отдельное рассмотрение |
proposal.id |
Непустая строка | Отсутствует | Вход не принят |
proposal.title |
Непустая после trim строка | Только пробелы | Запрос уточнения |
proposal.topic |
Строка | Массив значений | Ошибка формы |
Неизвестная строковая тема и неверный тип темы требуют разных объяснений. web3 является осмысленным предложением направления, которое пока не маршрутизируется. Массив вместо строки нарушает форму договора. Смешав эти случаи, мы либо потеряем пригодную идею, либо разрешим случайные преобразования неправильных данных.
Метод, заголовки и тело
Для учебного входа выбираем метод POST: отправитель сообщает о новом событии. Заголовок Content-Type: application/json описывает формат тела. Заголовок Accept сообщает, какой формат ответа предпочитает отправитель; он не заменяет Content-Type. Секрет авторизации, если появится настоящее подключение, должен передаваться предусмотренным механизмом, а не записываться в заголовок предлагаемой статьи.
Тело запроса не ограничено одним методом POST: например, операции PUT и PATCH также могут использовать тело. Конкретное поведение задаёт API, а не универсальное правило «всё содержимое относится к POST». В нашем договоре достаточно одной операции. Добавлять методы обновления, удаления и поиска сейчас означало бы расширять задачу без необходимости.
Учебный адрес https://automation.professorweb.example/intake показывает только структуру обращения. Он не настроен и не предназначен для отправки. Значения query-параметров не используются как замена JSON-полей: иначе появятся два источника заголовка или темы. Если в запросе есть и тело, и параметр topic, получатель не должен угадывать, какой из них правильный.
В реальном webhook важно отделять доступ к адресу от проверки заявки. Узел n8n поддерживает настройки аутентификации и разные варианты ответа, но их наличие ещё не описывает политику конкретной редакции. В нашем приложении auth_configured равно ложному, endpoint не создан. Документация Webhook описывает доступные параметры узла; учебный договор не является готовой защищённой интеграцией.
Что означает ответ получателя
Опишем ожидаемые ответы как часть проекта. При пригодной форме сообщается, что заявка принята для подготовки, а не одобрена редактором. При несовместимой структуре сообщается о проблемном поле. Успешный HTTP-ответ не должен содержать обещание публикации. Для P-001 результатом будет подготовка записи; для P-003 — ручная проверка темы.
В качестве проектного варианта используем 202 для принятого к дальнейшему рассмотрению события и 400 для нарушенной формы. Это ещё не реализованные ответы сервера. Не следует считать любой 4xx ошибкой человека, а любой 5xx доказательством недоступности внешнего сервиса: причины зависят от операции. Нужно читать код вместе с документированным телом и контекстом запроса.
Сразу определим, что идентификатор заявки не равен ключу гарантированной однократной обработки. Если отправитель повторит P-001, два запроса могут создать две попытки выполнения. Для будущего внешнего хранения понадобится договор о дедупликации и атомарной записи. В этой серии повтор выявляется в учебных данных; обещания exactly-once и работающей базы мы не вводим.
Предположим, отправитель не получил ответ из-за обрыва соединения. Он не знает, был ли запрос обработан. Повтор может быть оправдан, но должен сохранять идентичность события. Присвоение нового id при каждой сетевой попытке сделает дубликаты невидимыми. Это конкретная причина отделить идентификатор предложения от номера попытки или времени доставки.
Адаптер к внутренней форме
В приложении webhook-envelope.json расположен синтетический вход с body, внутри которого находится договор события. После адаптера внутренние узлы должны видеть proposal на одном и том же месте. Для ручного образца это означает явно создать такую же форму. Не используйте выражение «взять body, иначе весь объект» без описания допустимых вариантов: случайное поле body может скрыть неправильный вход.
Зафиксируем два известных источника: manual_fixture и webhook_envelope. Первый хранит событие непосредственно, второй — в body. Выбор источника задаётся отдельно; произвольный объект не считается автоматическим третьим вариантом. Это помогает объяснить ошибку, когда выражение ищет $json.proposal.title, а фактически данные находятся в $json.body.proposal.title.
Состояние урока сохраняет договор и ожидаемую адаптацию. Никакой HTTP-запрос не выполнен, ответа сервера в файлах нет. Проверьте по таблице вход P-001, затем замените только тип schema_version на строку: ожидается иной исход, хотя видимое значение похоже. В следующем уроке разнесём чтение входа и подготовку полей по узлам n8n.