Serverless-обработчик вебхуков с ретраями, логами и фоновыми задачами
Serverless-обработчику вебхуков нужны три вещи: проверить подпись по сырому телу, подтвердить в окне таймаута провайдера и продолжить тяжёлую работу в фоновом пайплайне, чтобы повторные доставки не дублировали побочные эффекты. Inquir собирает все три в изолированных контейнерных функциях за шлюзом, который контролируете вы.
Обновлено: 2026-06-28
- Проверка HMAC-подписи по сырому телу запроса до любого парсинга
- Быстрый 200 в окнах таймаута Stripe, GitHub, Slack и Shopify
- Передача в пайплайны или задачи для медленной работы на следующих шагах
- Ключи идемпотентности и трассы выполнения для повторных доставок
Кратко
Суть ответа
Serverless-обработчик вебхуков с ретраями, логами и фоновыми задачами. Одна функция на провайдера держит проверку подписи маленькой, обозримой и независимой. Функция проверяет HMAC по сырому телу, записывает ключ идемпотентности против повторной обработки, сразу возвращает 200 и ставит тяжёлую работу в пайплайн.
Когда подходит
- Вы получаете вебхуки от Stripe, GitHub, Slack, Shopify или любого провайдера с HMAC-подписью, и обработка должна быть проверенной, идемпотентной и безопасной для async.
- Нужны функции под конкретных провайдеров, изолированные от пользовательских API-маршрутов.
На что обратить внимание
- Выполнять всю мутацию синхронно до ответа 200 значит, что любая медлительность на следующем шаге — медленная база, медленный почтовый API, сетевой сбой — выглядит для провайдера как ошибка и запускает каскад повторных доставок.
- Один общий эндпоинт для Stripe, GitHub и Slack смешивает логику проверки и оси масштабирования, которые вы хотели развести.
Нагрузка и где ломается
Почему обработка вебхуков сложнее, чем кажется
SaaS-провайдеры повторяют доставку агрессивно. Stripe повторяет неудачный вебхук до 72 часов, GitHub — 3 дня, Slack ждёт ответ за 3 секунды, иначе помечает приложение медленным. Если обработчик перед ответом выполняет медленную мутацию в базе, рано или поздно вы увидите дубли побочных эффектов: двойные списания, двойные письма, двойные записи.
Пропустить проверку подписи, чтобы выкатиться быстрее, — самый частый срез углов. Кажется безобидным, пока кто-нибудь не переиграет событие Stripe payment_intent.succeeded против вашего продакшен-эндпоинта.
Компромиссы
Где ломается обработка вебхуков внутри запроса
Выполнять всю мутацию синхронно до ответа 200 значит, что любая медлительность на следующем шаге — медленная база, медленный почтовый API, сетевой сбой — выглядит для провайдера как ошибка и запускает каскад повторных доставок.
Один общий эндпоинт для Stripe, GitHub и Slack смешивает логику проверки и оси масштабирования, которые вы хотели развести.
Как помогает Inquir
Проверить, подтвердить, продолжить
Одна функция на провайдера держит проверку подписи маленькой, обозримой и независимой. Функция проверяет HMAC по сырому телу, записывает ключ идемпотентности против повторной обработки, сразу возвращает 200 и ставит тяжёлую работу в пайплайн.
Пайплайны работают в изолированных контейнерах с ретраями, трассами по шагам и общими секретами — у логики выполнения заказов Stripe та же наблюдаемость, что у HTTP API, а не чёрный ящик фонового воркера.
Что вы получаете
Возможности обработчика вебхуков
Проверка подписи на шлюзе (GitHub и Stripe)
Задайте webhookMode на маршруте — шлюз проверит HMAC по сырому телу до запуска функции. GitHub и Stripe встроены (для Stripe — с допуском по метке времени против replay), при несовпадении шлюз отвечает 403 BAD_SIGNATURE. Для других провайдеров body по-прежнему приходит строкой, и вы проверяете подписанные байты в обработчике.
Изолированные функции на провайдера
Сопоставьте каждому провайдеру свою функцию и маршрут. Логика Stripe не касается обработчиков GitHub; ревью безопасности сводится к одному небольшому файлу.
Передача в пайплайны
Быстро верните 200; выполнение заказа, письма или синхронизацию данных поставьте в шаг пайплайна, который работает вне HTTP-окна и не блокирует повторные доставки.
Идемпотентность и трассы выполнения
Записывайте ID событий до мутаций. Трассы в консоли показывают тело (с сокрытием чувствительного), заголовки, тайминги и число ретраев на каждую доставку.
Что дальше
Как собрать serverless-обработчик вебхуков на Inquir
Проверка по сырому телу, подтверждение до таймаута, идемпотентные записи.
Проверить подпись
Для GitHub и Stripe задайте webhookMode на маршруте — шлюз проверит сырое тело до запуска обработчика (403 при плохой подписи). Для других провайдеров читайте event.body как есть до любого JSON.parse и сравнивайте HMAC через timing-safe сравнение.
Записать ключ идемпотентности, вернуть 200
Сделайте upsert ID события провайдера до любой мутации состояния. Верните 200 в окне провайдера: Stripe ждёт < 30 с, Slack — < 3 с.
Поставить медленную работу в пайплайн
Для любой работы, которая не укладывается в окно таймаута, вызовите global.durable.startNew() и верните ссылку на задачу. Оркестрация ретраит независимо от HTTP-ответа.
Пример кода
Обработчики вебхуков Stripe и GitHub
Одна функция на провайдера. body приходит строкой — никогда не парсите до проверки. Используйте timing-safe сравнение, чтобы устоять против timing-атак на проверку HMAC.
import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export async function handler(event) { const rawBody = event.body ?? ''; const sig = event.headers['stripe-signature'] ?? ''; let evt; try { evt = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET); } catch (err) { return { statusCode: 400, body: `Webhook Error: ${err.message}` }; } const isNew = await db.upsertWebhookEvent(evt.id, evt.type); if (!isNew) return { statusCode: 200, body: 'duplicate' }; await global.durable.startNew('stripe-fulfillment', undefined, { eventId: evt.id, type: evt.type, data: evt.data.object }); return { statusCode: 200, body: 'accepted' }; }
import { createHmac, timingSafeEqual } from 'node:crypto'; export async function handler(event) { const body = event.body ?? ''; const sigHeader = (event.headers['x-hub-signature-256'] ?? '').replace('sha256=', ''); const expected = createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET).update(body).digest('hex'); if (sigHeader.length !== expected.length || !timingSafeEqual(Buffer.from(sigHeader, 'hex'), Buffer.from(expected, 'hex'))) { return { statusCode: 401, body: 'invalid signature' }; } const eventType = event.headers['x-github-event']; const payload = JSON.parse(body); if (eventType === 'push') { await global.durable.startNew('index-repo', undefined, { repo: payload.repository.full_name, sha: payload.after }); } return { statusCode: 200, body: 'accepted' }; }
Когда подходит
Когда паттерн уместен
Когда это уместно
- Вы получаете вебхуки от Stripe, GitHub, Slack, Shopify или любого провайдера с HMAC-подписью, и обработка должна быть проверенной, идемпотентной и безопасной для async.
- Нужны функции под конкретных провайдеров, изолированные от пользовательских API-маршрутов.
Когда лучше выбрать другое
- Вы потребляете вебхуки только через iPaaS вроде Zapier или Workato и не трогаете рантайм.
Частые вопросы
Частые вопросы
Зачем проверять подпись до парсинга?
HMAC считается по сырым байтам в исходном виде. Если сначала сделать JSON.parse и сериализовать заново, пробелы и порядок ключей могут измениться — валидные события не пройдут проверку. Всегда читайте event.body строкой до вызова любой HMAC-функции.
Как уложиться в 3-секундный лимит Slack?
Сразу верните 200 с пустым телом. Саму работу поставьте в шаг пайплайна. Если нужно ответить в Slack, используйте responseUrl из payload slash-команды внутри шага пайплайна.
Как защититься от replay-атак?
Сочетайте проверку HMAC, метки времени провайдера (в пределах ±5 минут) и ключи идемпотентности — запоздалые повторы отклоняются, а повторные доставки не дают эффекта.
Можно тестировать локально?
Используйте Stripe CLI stripe listen --forward-to или похожие инструменты, чтобы пробрасывать реальные события на локальный обработчик. Сохраняйте тот же контракт event.body строкой, который шлюз отдаёт в проде.