Урок 07.07: Webhooks — проектирование событийных интеграций
Цель урока
Разобраться, как устроены webhook-интеграции («обратные вызовы по HTTP»): чем они отличаются от polling и брокеров сообщений, как проектировать подписку на события, проверять подлинность входящих запросов и обеспечивать надёжную обработку при повторных доставках. Особый фокус: разбор реальной катастрофы с дублированием платежа из-за неправильной обработки webhook, а также описание webhook-событий в OpenAPI 3.1.
Ключевые понятия
| Термин | Определение |
|---|---|
| Webhook | HTTP-запрос (обычно POST), который система-источник отправляет на заранее зарегистрированный URL при возникновении события — «обратный вызов» |
| Callback URL / Endpoint | URL приёмной системы, на который источник отправляет webhook |
| Событие (Event) | Факт, произошедший в системе-источнике (task.created, payment.succeeded) |
| Payload | Тело webhook-запроса — данные о событии в формате JSON |
| Подписка (Subscription) | Регистрация: «отправляй мне события типов X, Y, Z на этот URL» |
| Endpoint Verification | Проверка, что приёмный URL реально принадлежит подписчику (challenge-response) |
| Подпись (Signature) | HMAC-хэш payload'а с секретным ключом — доказывает, что запрос отправил легитимный источник |
| Event ID | Уникальный идентификатор события — основа для дедупликации на стороне приёмника |
| Idempotency (на приёмнике) | Свойство обработчика: повторная доставка того же события не создаёт дублирующих эффектов |
| Retry Policy | Правила повторной отправки webhook, если приёмник не ответил 2xx |
| At-least-once delivery | Гарантия: событие будет доставлено хотя бы один раз, возможно — несколько |
| Dead Letter (для webhook) | Список событий, которые источник не смог доставить после всех retry — обычно доступен в дашборде провайдера |
1. Что такое Webhook и почему он нужен
1.1. Аналогия: webhook — это «обратный звонок», а не «звонок в справочную»
Представьте, что вы заказали пиццу и ждёте, когда она будет готова. Есть два варианта:
- Polling («справочная»): вы каждые 2 минуты звоните в пиццерию и спрашиваете «готово?». 99% звонков — впустую, но зато вы точно не пропустите момент готовности (с задержкой до 2 минут).
- Webhook («обратный звонок»): вы оставляете номер телефона, и пиццерия сама звонит вам, когда пицца готова. Ноль лишних запросов, событие приходит почти мгновенно.
Webhook — это «обратный звонок» в мире HTTP: система-источник (пиццерия) делает исходящий HTTP-запрос на URL системы-получателя (ваш телефон), когда что-то произошло.
1.2. Зачем аналитику знать webhooks
Webhooks — один из самых частых видов реальных интеграций:
- Платёжная система уведомляет интернет-магазин: «оплата прошла» (
payment.succeeded) - CRM уведомляет внешний сервис: «создан новый лид»
- GitHub уведомляет CI/CD: «новый push в репозиторий»
- Telegram-бот получает webhook при новом сообщении пользователю
- 1С обменивается событиями с внешним складом через HTTP-callback
Когда аналитик пишет ТЗ на такую интеграцию, именно от него зависит, будут ли в требованиях прописаны: проверка подписи, обработка дублей, поведение при недоступности приёмника. Если этого нет в ТЗ — разработчик сделает «как получится», и систему ждут проблемы уровня катастрофы из раздела 5.
1.3. Webhook vs Polling vs WebSocket vs SSE
| Критерий | Webhook | Polling | WebSocket | SSE (Server-Sent Events) |
|---|---|---|---|---|
| Кто инициирует соединение | Источник делает HTTP-запрос приёмнику | Приёмник периодически опрашивает источник | Клиент открывает persistent-соединение | Клиент открывает persistent HTTP-соединение |
| Реал-тайм | Почти мгновенно (зависит от источника) | Задержка = интервал опроса | Мгновенно, двусторонне | Мгновенно, односторонне (сервер → клиент) |
| Нагрузка на источник | Низкая (1 запрос = 1 событие) | Высокая (много «пустых» запросов) | Средняя (держит соединения открытыми) | Средняя |
| Требования к приёмнику | Нужен публичный HTTPS-endpoint, который всегда доступен | Нет — приёмник сам стучится | Нужна поддержка WS-протокола | Нужна поддержка SSE на клиенте |
| Типичный пример | Stripe → магазин: payment.succeeded |
Старый бот: «проверять новые заказы раз в минуту» | Чат, биржевые котировки | Лента уведомлений в браузере |
Для аналитика: если в ТЗ написано «система Б должна узнавать об изменениях в системе А почти мгновенно, без постоянного опроса» — это требование к webhook-интеграции. Если же система Б — это, например, десктопное приложение за NAT без публичного адреса, webhook physически невозможен, и нужен polling или брокер сообщений (см. Урок 07.06).
2. Анатомия webhook-запроса
2.1. HTTP-запрос: что приходит на endpoint
Источник отправляет обычный POST-запрос с JSON-телом:
POST /webhooks/task-manager HTTP/1.1
Host: partner-system.example.com
Content-Type: application/json
X-Webhook-Id: evt_8f3a1c2b
X-Webhook-Signature: sha256=4f6b3c2e8a1d...
X-Webhook-Timestamp: 1718194800
User-Agent: TaskManager-Webhooks/1.0
{
"event_id": "evt_8f3a1c2b",
"event_type": "task.created",
"occurred_at": "2026-06-12T10:00:00Z",
"data": {
"task_id": 4521,
"title": "Подготовить отчёт",
"created_at": "2026-06-12T10:00:00Z"
}
}
Это та же модель TaskCreatedEvent, которая описывается через oneOf + discriminator в Уроке 07.02 — webhook-payload и есть конкретная JSON-схема из этого oneOf.
2.2. Конверт события (Event Envelope)
Хороший webhook-payload разделяет метаданные о событии и данные о сущности:
| Поле | Назначение |
|---|---|
event_id |
Уникальный ID самого события — основа для дедупликации (раздел 5.2) |
event_type |
Тип события (task.created, payment.succeeded) — определяет структуру data |
occurred_at |
Когда событие произошло в системе-источнике (не когда отправлен webhook!) |
data |
Полезная нагрузка — сущность, к которой относится событие |
Антипаттерн, который часто встречается в плохо спроектированных API: вместо конверта в payload присылают «голую» сущность без event_id и event_type. Тогда приёмник не может ни определить тип события без анализа структуры, ни дедуплицировать повторные доставки.
3. Жизненный цикл webhook-подписки
3.1. Регистрация endpoint'а
Приёмник должен зарегистрировать свой URL и список событий, на которые он подписывается:
POST /api/v1/webhook-subscriptions
Content-Type: application/json
Authorization: Bearer <token>
{
"url": "https://partner-system.example.com/webhooks/task-manager",
"events": ["task.created", "task.updated", "task.deleted"],
"secret": "whsec_3f8a1c..."
}
secret — общий секрет для подписи (раздел 4.1). Он генерируется один раз и хранится только на сервере источника и в защищённой конфигурации приёмника — никогда не передаётся в открытом виде после первоначальной настройки.
3.2. Endpoint Verification (challenge-response)
Перед тем как начать слать реальные события, источник должен убедиться, что URL действительно принадлежит подписчику (а не указан по ошибке или злонамеренно — иначе можно «заDDoSить» чужой сервер чужими webhook'ами).
Типичная схема (используется Slack, Stripe и др.):
- Источник отправляет на новый URL специальный запрос с одноразовым токеном:
{ "type": "endpoint.verification", "challenge": "a1b2c3d4e5f6" } - Приёмник должен вернуть этот
challengeобратно (например, в теле ответа или заголовке) в течение ограниченного времени. - Только после успешной верификации подписка переходит в статус
active.
Для аналитика: если в системе есть UI для управления подписками, в нём обязательно должен отображаться статус (pending_verification / active / failed / disabled) — иначе пользователь не поймёт, почему события не приходят.
3.3. Управление подпиской: фильтры и версии событий
Хорошо спроектированный webhook-сервис позволяет:
- Подписываться выборочно на типы событий (а не «на всё подряд» — это лишняя нагрузка и риски безопасности)
- Иметь несколько подписок с разными URL под разные цели (например, отдельный endpoint для аналитики, отдельный — для уведомлений)
- Версионировать структуру payload (
event_type: task.created.v2) — без этого любое изменение формата события ломает всех подписчиков одновременно, аналогично проблеме версионирования REST API из Урока 07.01
4. Безопасность webhook-интеграций
Webhook — это входящий запрос из интернета на ваш сервер. Без проверки подлинности любой человек, узнавший URL, может отправить поддельный webhook (например, «оплата прошла» без реальной оплаты).
4.1. Подпись запроса (HMAC-SHA256)
Источник подписывает тело запроса общим секретом и передаёт подпись в заголовке:
X-Webhook-Signature: sha256=4f6b3c2e8a1d9f0e7c5b3a2d1e8f6c4b...
Алгоритм проверки на стороне приёмника:
- Взять «сырое» (raw) тело запроса до парсинга JSON (важно: после
JSON.parse → JSON.stringifyбайты могут отличаться, и подпись не совпадёт) - Вычислить
HMAC-SHA256(secret, raw_body) - Сравнить с значением из
X-Webhook-Signature— с использованием constant-time сравнения (crypto.timingSafeEqual), чтобы не дать возможность подобрать подпись по времени ответа - Если подписи не совпадают — вернуть
401 Unauthorizedи не обрабатывать payload
# Псевдокод проверки подписи
expected = HMAC_SHA256(secret_key, raw_request_body)
received = headers["X-Webhook-Signature"].replace("sha256=", "")
if not constant_time_equals(expected, received):
return 401 Unauthorized
# подпись верна — можно обрабатывать payload
Для аналитика: в требованиях к интеграции должно быть явно указано: «входящие webhook должны проверяться по подписи HMAC-SHA256, секрет хранится в защищённом хранилище (Vault/Secrets Manager), запросы без валидной подписи или с истёкшим timestamp отклоняются с кодом 401». Без этого пункта проверка подписи может быть «забыта» при реализации.
4.2. Защита от replay-атак: проверка timestamp
Даже с корректной подписью злоумышленник, перехвативший один легитимный webhook, может отправлять его повторно (replay attack) — подпись останется валидной, потому что она считается от тех же данных.
Решение: в payload или заголовок добавляется X-Webhook-Timestamp, который тоже входит в подписываемые данные. Приёмник проверяет:
if abs(current_time - timestamp) > 300 секунд:
return 401 # запрос слишком старый — возможен replay
4.3. Дополнительные меры
| Мера | Когда нужна |
|---|---|
| IP allowlisting | Если источник публикует фиксированный список IP-адресов (например, у Stripe и GitHub такие списки есть) — дополнительный, но не единственный слой защиты |
| mTLS (mutual TLS) | Для интеграций между корпоративными системами (банки, госсистемы) — обе стороны предъявляют сертификаты |
| Отдельный секрет на каждую подписку | Чтобы компрометация одного приёмника не давала доступ к подделке webhook для других подписчиков |
4.4. Чек-лист безопасности webhook
| № | Проверка |
|---|---|
| 1 | Подпись (HMAC) проверяется до обработки payload |
| 2 | Секрет хранится в Secrets Manager, не в коде/репозитории |
| 3 | Проверяется timestamp (защита от replay), окно — не более 5 минут |
| 4 | Сравнение подписи — constant-time |
| 5 | Endpoint доступен только по HTTPS |
| 6 | На запрос без валидной подписи — 401, без подробностей в ответе (не подсказывать атакующему, что не так) |
5. ⚠️ КАТАСТРОФА: Двойная отправка заказа из-за дублирующегося webhook
5.1. Реальная история: «Покупатель оплатил один раз — заказ собрали и отправили дважды»
Контекст: интернет-магазин принимает оплату через внешнюю платёжную систему. После успешной оплаты платёжная система отправляет webhook payment.succeeded, и магазин по этому событию: списывает товар со склада, создаёт заявку на доставку и отправляет покупателю email с подтверждением.
Реализация обработчика (как было сделано):
POST /webhooks/payments
1. Проверить подпись (~5 мс)
2. Найти заказ по order_id (~20 мс)
3. Списать товар со склада (вызов Складской системы) (~2000 мс)
4. Создать заявку в Службе доставки (внешний вызов) (~2500 мс)
5. Отправить email через SMTP (~1500 мс)
6. Вернуть 200 OK
ИТОГО: ~6 секунд
Что произошло:
- Платёжная система отправила webhook
payment.succeededдля заказа №10532. - Магазин начал обработку (склад → доставка → email) — это заняло 6 секунд.
- У платёжной системы таймаут ожидания ответа — 5 секунд. Не получив
200 OKза 5 секунд, она посчитала доставку неуспешной. - Платёжная система повторила webhook (retry #1) через 10 секунд.
- К этому моменту первая обработка ещё не завершилась (склад был «подвисшим» из-за нагрузки) — но запрос retry #1 запустил вторую, параллельную обработку того же события.
- В итоге: товар списан со склада два раза (на складе образовался отрицательный остаток), создано две заявки на доставку, покупателю пришло два письма.
5.2. Sequence-диаграмма катастрофы
@startuml
participant "Платёжная\nсистема" as PSP
participant "Webhook-\nобработчик" as Handler
participant "Склад" as Warehouse
participant "Доставка" as Delivery
PSP -> Handler: POST /webhooks/payments\n(event_id=evt_001)
activate Handler
Handler -> Warehouse: списать товар
activate Warehouse
note right: 2 секунды...
PSP --> PSP: timeout 5 сек, нет ответа
PSP -> Handler: RETRY: POST /webhooks/payments\n(event_id=evt_001, тот же payload)
activate Handler #LightCoral
note right of Handler: Второй обработчик\nстартует параллельно!\nНет проверки event_id
Warehouse --> Handler: ok (списано #1)
deactivate Warehouse
Handler -> Delivery: создать заявку #1
Handler -> Warehouse: списать товар (повторно!)
activate Warehouse #LightCoral
Warehouse --> Handler: ok (списано #2 — минус на складе)
deactivate Warehouse
Handler -> Delivery: создать заявку #2 (дубль)
deactivate Handler
deactivate Handler
@enduml
5.3. Бизнес-последствия
- Покупатель получил два письма с подтверждением — потерял доверие («магазин ничего не контролирует»)
- Создано два заказа на доставку — расходы на логистику ×2
- Отрицательный остаток на складе — некорректные данные для следующего покупателя («товар в наличии», хотя его уже нет)
- Финансовый отдел зафиксировал расхождение между «оплачено один раз» и «отгружено два раза»
5.4. Анатомия ошибки: что было упущено
| № | Что было упущено | К какому разделу относится |
|---|---|---|
| 1 | Обработчик не отвечал 200 OK немедленно — синхронно делал всю тяжёлую работу |
Раздел 5.5 |
| 2 | Не было дедупликации по event_id — повторная доставка обработана как новое событие |
Раздел 5.6 |
| 3 | Webhook-провайдер использует at-least-once доставку — дубли это норма, а не баг провайдера | Раздел 5.6 |
5.5. Как правильно: быстрый ACK + асинхронная обработка
Принцип: обработчик webhook должен сразу (за миллисекунды) сохранить событие и ответить 200 OK, а тяжёлую обработку выполнить асинхронно — например, положив задачу в очередь (см. Урок 07.06).
POST /webhooks/payments
1. Проверить подпись (~5 мс)
2. Проверить event_id на дубликат (см. 5.6) (~5 мс)
3. Сохранить событие в таблицу webhook_events (~10 мс)
4. Поставить задачу в очередь на обработку (~5 мс)
5. Вернуть 200 OK (~25 мс ИТОГО)
--- асинхронно, в очереди ---
6. Списать товар, создать заявку на доставку, отправить email
Теперь даже если асинхронная обработка займёт 6 секунд — платёжная система получит 200 OK за 25 мс и не станет повторять запрос.
5.6. Как правильно: дедупликация по event_id
Даже с быстрым 200 OK дубликаты возможны (сетевые сбои, retry после потерянного ответа). Поэтому обработчик должен быть идемпотентным:
-- Таблица для дедупликации
CREATE TABLE webhook_events (
event_id VARCHAR(64) PRIMARY KEY,
event_type VARCHAR(64),
received_at TIMESTAMP,
status VARCHAR(16) -- pending | processed | failed
);
INSERT INTO webhook_events (event_id, event_type, received_at, status)
VALUES ($1, $2, now(), 'pending')
ON CONFLICT (event_id) DO NOTHING;
-- если строка не была вставлена (конфликт) — событие уже видели,
-- сразу вернуть 200 OK без повторной обработки
Это тот же принцип Idempotency-Key, который разбирается для исходящих запросов в Уроке 07.05 — здесь он применяется к входящим webhook.
5.7. Как аналитик может предотвратить такую катастрофу
В ТЗ на webhook-интеграцию должно быть явно прописано:
- Контракт ответа: обработчик обязан вернуть
2xxза не более N секунд (узнать у провайдера его timeout — обычно 3-10 сек) - Архитектурное требование: тяжёлая бизнес-логика выполняется асинхронно, после немедленного
200 OK - Идемпотентность: обязательная дедупликация по
event_id, дублирующиеся события игнорируются молча (с логированием) - Поведение при ошибке: если асинхронная обработка упала — что происходит? (retry из локальной очереди, алерт, ручная обработка из DLQ)
6. Webhooks в OpenAPI 3.1
6.1. Два механизма: webhooks и callbacks
OpenAPI различает два похожих, но разных понятия:
webhooks (top-level, начиная с 3.1) |
callbacks (на уровне операции, с 3.0) |
|
|---|---|---|
| Что описывает | Все события, которые API может прислать подписчику, независимо от конкретного запроса | Callback, привязанный к конкретной операции («дай мне URL — я вызову его, когда выполню твою задачу») |
| Пример | «Этот сервис умеет слать task.created, task.updated, payment.succeeded» |
«При POST /reports укажи callbackUrl — туда придёт результат генерации отчёта» |
| URL подписчика | Настраивается отдельно (через subscription API), в спеке не привязан к конкретному запросу | Берётся из данных самого запроса через runtime expression |
6.2. Пример: секция webhooks (OpenAPI 3.1)
Используя схему WebhookEvent (oneOf + discriminator) из Урока 07.02, описание исходящих webhook выглядит так:
openapi: 3.1.0
info:
title: Task Manager API
version: 1.0.0
webhooks:
taskEvent:
post:
summary: Уведомление о событии задачи
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/WebhookEvent"
responses:
"200":
description: Webhook принят
"401":
description: Неверная подпись
components:
schemas:
WebhookEvent:
oneOf:
- $ref: "#/components/schemas/TaskCreatedEvent"
- $ref: "#/components/schemas/TaskUpdatedEvent"
discriminator:
propertyName: event_type
mapping:
task.created: "#/components/schemas/TaskCreatedEvent"
task.updated: "#/components/schemas/TaskUpdatedEvent"
6.3. Пример: секция callbacks (асинхронный результат операции)
paths:
/reports:
post:
summary: Запросить генерацию отчёта (асинхронно)
requestBody:
content:
application/json:
schema:
type: object
properties:
callbackUrl:
type: string
format: uri
example: "https://partner.example.com/hooks/report-ready"
responses:
"202":
description: Запрос принят, отчёт будет сгенерирован асинхронно
callbacks:
reportReady:
"{$request.body#/callbackUrl}":
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
report_id: { type: string }
status: { type: string, enum: [ready, failed] }
download_url: { type: string, format: uri }
responses:
"200":
description: Подтверждение получения
Runtime-выражение {$request.body#/callbackUrl} означает: «URL для callback берётся из поля callbackUrl тела исходного запроса».
Для аналитика: если в требованиях есть фраза «после завершения долгой операции система пришлёт результат на указанный URL» — это callbacks. Если «система в принципе может присылать события подписчикам» — это webhooks.
7. Тестирование webhook-интеграций
7.1. Проблема: как протестировать входящий webhook на локальной машине
Источник webhook находится во внешней системе (часто — в облаке) и не может «достучаться» до localhost. Для тестирования нужен публично доступный URL, который проксирует запросы на локальную машину.
| Инструмент | Назначение |
|---|---|
| webhook.site | Бесплатный сервис: даёт временный публичный URL, показывает все входящие запросы (заголовки, тело) — удобно для первичного изучения формата payload от незнакомого провайдера |
| ngrok / localtunnel | Создают публичный туннель к localhost — разработчик может принимать реальные webhook локально во время отладки |
| Postman Mock Server | Можно создать mock-endpoint, который принимает webhook и возвращает заданный ответ — полезно для тестирования retry-логики источника (см. Урок 07.08) |
7.2. Чек-лист тестирования webhook-интеграции
| № | Проверка |
|---|---|
| 1 | Endpoint отвечает 2xx при валидном payload и подписи |
| 2 | Endpoint отвечает 401 при отсутствующей/неверной подписи |
| 3 | Endpoint отвечает 401 при устаревшем timestamp (replay) |
| 4 | Повторная отправка того же event_id не создаёт дублирующих эффектов |
| 5 | Endpoint отвечает быстро (< таймаута провайдера), даже если бизнес-логика медленная |
| 6 | Обработка некорректного event_type (новый/неизвестный тип события) не приводит к ошибке 500 |
| 7 | Endpoint Verification (challenge-response) проходит при регистрации подписки |
8. Чек-лист аналитика при проектировании webhook-интеграции
| № | Проверка | Раздел |
|---|---|---|
| 1 | Определён список событий (event_type) и структура data для каждого |
2 |
| 2 | Описан формат конверта события (event_id, event_type, occurred_at, data) |
2.2 |
| 3 | Описан процесс регистрации подписки и endpoint verification | 3 |
| 4 | Указан механизм версионирования событий | 3.3 |
| 5 | Указано требование к подписи запроса (HMAC-SHA256) и где хранится секрет | 4.1 |
| 6 | Указана защита от replay (timestamp + окно) |
4.2 |
| 7 | Указан таймаут, за который обработчик должен ответить 2xx |
5.5 |
| 8 | Указано, что тяжёлая логика выполняется асинхронно после 200 OK |
5.5 |
| 9 | Указана дедупликация по event_id (идемпотентность обработчика) |
5.6 |
| 10 | Описана retry-политика источника (сколько попыток, интервалы) | 5 |
| 11 | Описано поведение при исчерпании retry (Dead Letter / алерт / ручная обработка) | — |
| 12 | Указано, гарантируется ли порядок доставки событий (как правило — нет) | 3.3 |
| 13 | Endpoint доступен только по HTTPS | 4.3 |
| 14 | Предусмотрен мониторинг: сколько webhook доставлено / отклонено / в очереди на retry | — |
Вопросы для самопроверки
Базовый уровень
- Чем webhook отличается от polling? Приведите пример ситуации, где webhook невозможен.
- Что такое «конверт события» (event envelope)? Какие поля в нём обязательны и почему?
- Зачем нужна подпись (HMAC) webhook-запроса? Что произойдёт, если её не проверять?
- Что такое Endpoint Verification и зачем она нужна перед началом отправки реальных событий?
- В чём разница между
webhooksиcallbacksв OpenAPI 3.1?
Продвинутый уровень
- At-least-once: Почему webhook-провайдеры почти всегда гарантируют «доставлено хотя бы один раз», а не «ровно один раз»? Что это значит для приёмника?
- Replay-атака: Злоумышленник перехватил легитимный webhook
payment.succeededс валидной подписью и повторно отправляет его на endpoint магазина каждый час. Подпись каждый раз верна. Как это остановить? - Таймаут провайдера: У провайдера webhook таймаут ответа — 3 секунды, а обработка заказа в вашей системе занимает 8 секунд. Спроектируйте обработчик так, чтобы провайдер не считал доставку неуспешной.
- Версионирование событий: Источник изменил структуру
dataдляtask.updated(убрал полеold_status, добавилchanges: []). У вас 5 подписчиков, использующих старый формат. Как внедрить изменение без даунтайма для подписчиков? - Порядок событий: Подписчик получил
task.deletedдля задачи №42 раньше, чемtask.createdдля той же задачи (из-за повторной доставки и сетевых задержек). К каким проблемам это приведёт и как их избежать?
Практическое задание
Задание 1. Анализ payload и проектирование конверта события (1,5 балла)
Источник присылает webhook в следующем «голом» виде (без конверта):
{
"task_id": 4521,
"title": "Подготовить отчёт",
"status": "done",
"previous_status": "in_progress"
}
- (0,5 балла) Назовите минимум 3 проблемы такого формата с точки зрения приёмника.
- (0,5 балла) Перепроектируйте payload, добавив конверт события (
event_id,event_type,occurred_at,data). Укажите подходящийevent_type. - (0,5 балла) Опишите, какое поле и почему должно использоваться для дедупликации повторных доставок.
Задание 2. Проверка подписи: пошаговый алгоритм (2 балла)
Платёжная система присылает webhook с заголовками X-Webhook-Signature: sha256=<hex> и X-Webhook-Timestamp: <unix_time>. Секрет подписи: whsec_abc123.
- (1 балл) Опишите пошаговый алгоритм проверки запроса на стороне приёмника (от получения запроса до решения «обработать» или «отклонить»). Укажите конкретные коды ответа для каждого случая отказа.
- (0,5 балла) Почему важно вычислять HMAC от raw body, а не от объекта после
JSON.parse? - (0,5 балла) Почему сравнение подписей должно быть constant-time? Что произойдёт, если использовать обычное
==?
Задание 3. Кейс «Двойная отправка заказа» — анализ катастрофы (2,5 балла)
Вернитесь к катастрофе из раздела 5. Магазин получил webhook payment.succeeded, обработка заняла 6 секунд, провайдер сделал retry через 5 секунд, в результате заказ обработан дважды.
- (0,5 балла) Кто допустил ошибку — провайдер платежей (отправил retry) или магазин (медленно обработал)? Обоснуйте.
- (0,5 балла) Опишите изменение архитектуры обработчика (что происходит синхронно, что асинхронно).
- (0,5 балла) Опишите механизм, который предотвратит повторную обработку, даже если retry всё же произойдёт.
- (0,5 балла) Что должно произойти с заявкой на доставку и письмом, если обработка события упала с ошибкой на этапе создания заявки (после списания со склада)? Опишите стратегию восстановления.
- (0,5 балла) Какие 3 пункта вы добавите в ТЗ на эту интеграцию, чтобы такая катастрофа не повторилась на следующем проекте?
Задание 4. OpenAPI: описание webhook-события (2 балла)
Для Task Manager (см. Урок 07.02) система отправляет подписчикам событие task.assigned при назначении задачи на пользователя.
- (1 балл) Напишите схему
TaskAssignedEvent(конверт +dataс полямиtask_id,assignee_id,assigned_by,assigned_at) и добавьте её вoneOfсхемыWebhookEventс соответствующимdiscriminator.mapping. - (1 балл) Опишите секцию
webhooksв OpenAPI 3.1 для этого события, включаяrequestBodyи ответы200/401.
Задание 5. Retry-политика и чек-лист (2 балла)
Вы проектируете интеграцию, в которой ваша система — источник webhook (отправляет события внешним подписчикам).
- (1 балл) Спроектируйте таблицу retry-политики: сколько попыток, с какими интервалами (например, экспоненциальный backoff), что происходит после последней неуспешной попытки. Свяжите с подходами Retry из Урока 07.05.
- (1 балл) Опишите, какую информацию о доставке webhook должна видеть служба поддержки, если подписчик жалуется «мы не получили событие N часов назад» (минимум 4 пункта: что логировать и где это отображать).
Критерии оценки
| Задание | Баллы |
|---|---|
| Задание 1: Конверт события | 1,5 |
| Задание 2: Проверка подписи | 2 |
| Задание 3: Кейс «Двойная отправка» | 2,5 |
| Задание 4: OpenAPI webhook | 2 |
| Задание 5: Retry-политика | 2 |
| Итого | 10 |
Дополнительные материалы
- Стандарт: OpenAPI 3.1 Specification — секции
webhooksиcallbacks - Статья: «Webhooks: How to design a robust webhook system» (Hookdeck / Svix engineering blogs)
- Документация: Stripe Webhooks — пример подписи (
Stripe-Signature), retry-политики и Dead Letter в дашборде - Документация: GitHub Webhooks — пример Endpoint Verification и
X-Hub-Signature-256 - Инструмент: webhook.site — изучение формата входящих webhook
- Инструмент: ngrok — туннелирование для локальной отладки webhook
- Связанные темы: Урок 07.05 — Idempotency-Key и Retry, Урок 07.06 — Dead Letter Queue