Как тестировать вебхуки, ничего не разворачивая
Отладка вебхуков через одноразовый адрес: что провайдеры шлют на самом деле, почему падает проверка подписи и на чём теряют больше всего времени.
Самое неприятное в вебхуках — порядок действий. Нужен публичный адрес, иначе провайдер ничего не пришлёт. Нужно увидеть, что он присылает, иначе не написать обработчик. И вот человек выкатывает заглушку, смотрит логи, перевыкатывает, снова смотрит логи и тратит сорок минут на вопрос, который решается за две: как вообще выглядит этот payload?
Одноразовый адрес разрывает круг. Вставили его в панель провайдера, нажали «отправить тестовое событие» — и запрос уже перед вами, ещё до того как открыт редактор.
Сначала прочитать запрос, потом писать обработчик
Документация обычно точна насчёт структуры тела и расплывчата насчёт всего, что его окружает. Сюрпризы живут в заголовках.
Stripe кладёт подпись в Stripe-Signature, и это не обычный hex-хеш, а составная строка с меткой времени и одним или несколькими значениями v1=. GitHub шлёт X-Hub-Signature-256 и отдельно X-GitHub-Event с типом события: пропустите второй заголовок — и придётся угадывать по телу, push это или pull request. Shopify кодирует HMAC в base64, а не в hex. Ничего из этого не спрятано, но прочитать в документации и увидеть на живом запросе — разные степени понимания.
Перехватите одну доставку, и всё придёт сразу: метод, путь, строка запроса, заголовки, тело.
Ошибка с подписью, на которой спотыкаются все
Вот та, что стоит человеку половины рабочего дня. Проверка подписи требует сырого тела запроса — ровно тех байтов, которые хешировал провайдер. Если фреймворк разобрал JSON, а вы для проверки собрали его обратно, порядок ключей или пробел сдвинутся на символ, и хеш перестанет сходиться.
В Express express.json() вычитывает поток до того, как обработчик его увидит. Лечится сохранением копии:
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; },
}));
Дальше хешируем req.rawBody, а не JSON.stringify(req.body). В Fastify, Django и Rails есть свои версии той же ловушки.
Повторные доставки — не мелочь
Ответили не 2xx — и большинство провайдеров пришлёт событие снова. Stripe повторяет боевые события с нарастающими паузами до трёх суток. GitHub не повторяет вообще, зато позволяет переотправить вручную из настроек вебхука в репозитории.
Эта разница влияет на устройство обработчика. Если эндпоинт тормозит и уходит в таймаут, Stripe завалит вас дублями события, которое вы, возможно, уже обработали. Поэтому в любом серьёзном обработчике идентификатор события сохраняется, а повторы игнорируются. Перехват показывает, с какой скоростью прилетают ретраи и что меняется между попытками (обычно ничего, кроме идентификатора доставки).
Content-Type врёт
Сервисов, которые шлют JSON с заголовком Content-Type: text/plain, хватает. Старых интеграций, которые кладут JSON-строку в одно поле формы, тоже. Надёжнее смотреть на само тело, а не на заголовок. Тестер вебхуков так и делает: JSON, объявленный как обычный текст, всё равно выводится с отступами.
Одно правило стоит соблюдать: шлите тестовые события, а не настоящие. Публичный приёмник публичен, прочитать пришедшее может любой, у кого есть ссылка. Боевым ключам и данным клиентов там не место.
Возьмите адрес в Тестере вебхуков и отправьте первую тестовую доставку — за полминуты узнаете больше, чем документация расскажет за десять минут.