Serverless-обработчик вебхуков с ретраями, логами и фоновыми задачами
Паттерн: проверьте HMAC по сырому телу, ответьте 200 в лимите провайдера и передайте тяжёлую работу в пайплайн. Идемпотентность ловит повторные доставки; downstream повторяется на уровне шагов.
Обновлено: 2026-06-28
- HMAC-проверка по сырому телу до любого парсинга
- Быстрый 200 ACK в лимитах Stripe, GitHub, Slack и Shopify
- Async-handoff в пайплайны для медленной downstream-работы
- Ключи идемпотентности и трассировки при повторных доставках
Кратко
Суть ответа
Serverless-обработчик вебхуков с ретраями, логами и фоновыми задачами. Функция проверяет HMAC и сохраняет ключ идемпотентности, возвращает 200, вызывает global.durable.startNew(). Оркестрация продолжает работу вне HTTP-окна.
Когда подходит и когда нет
- Stripe, GitHub, Slack, Shopify или другой HMAC-провайдер шлёт вебхуки — нужна проверка, идемпотентность и async-безопасность.
- Нужны отдельные функции провайдера, изолированные от пользовательских API-маршрутов.
На что обратить внимание
- Синхронная обработка внутри HTTP-запроса гарантирует таймаут при росте нагрузки или медленном downstream.
- Без ключа идемпотентности повторные доставки провайдера создают дубли в базе или дублируют вызовы API.
Ситуация: нагрузка и где обычно ломается
Почему вебхуки теряются или дублируются
Если обработчик отвечает 200 только после тяжёлой работы, таймаут провайдера вызывает повторную доставку — и задача выполняется дважды.
Без проверки подписи любой запрос со знакомым URL может сымитировать вебхук и вызвать побочные эффекты.
Компромиссы
Где ломается упрощённая обработка вебхуков
Синхронная обработка внутри HTTP-запроса гарантирует таймаут при росте нагрузки или медленном downstream.
Без ключа идемпотентности повторные доставки провайдера создают дубли в базе или дублируют вызовы API.
Как Inquir помогает в этом сценарии
Проверить, ответить, продолжить в пайплайне
Функция проверяет HMAC и сохраняет ключ идемпотентности, возвращает 200, вызывает global.durable.startNew(). Оркестрация продолжает работу вне HTTP-окна.
Один шлюз обслуживает вебхуки Stripe, GitHub, Slack с раздельными маршрутами и API-ключами — без дополнительного nginx.
Что вы получаете на платформе
Что нужно надёжному обработчику вебхуков
Проверка подписи
Timing-safe сравнение HMAC SHA-256 с секретом провайдера из переменных окружения функции.
Быстрый ACK
Ответьте 200 до тяжёлой работы — иначе таймаут провайдера вызовет повторную доставку.
Ключ идемпотентности
Проверить ID доставки перед записью; повторная доставка не создаёт дубль.
Запуск оркестрации
global.durable.startNew() запускает тяжёлую работу асинхронно с теми же логами и секретами.
Что сделать дальше, по шагам
Как построить обработчик вебхука на Inquir
Проверка по сырому телу, ACK до таймаута, идемпотентные записи.
Проверить подпись
Сравните HMAC из заголовка с вычисленным значением — до любой другой работы.
Ответить 200 быстро
Запишите ключ идемпотентности и вызовите global.durable.startNew() до возврата ответа.
Обработать событие в пайплайне
Шаги пайплайна выполняют тяжёлую работу: API-вызовы, запись в БД, нотификации.
Пример кода
Stripe and GitHub webhook handlers
One function per provider. body comes in as a string — never parse before verifying. Use timing-safe comparison to resist timing attacks on HMAC checks.
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 — вы не трогаете runtime.
Вопросы и ответы
Вопросы и ответы
Зачем проверять подпись до парсинга?
HMAC считается по сырым байтам в исходном виде. JSON.parse до проверки и повторная сериализация меняют пробелы и порядок ключей — валидные события могут не пройти проверку. Читайте event.body строкой до вызова HMAC.
Как уложиться в 3-секундный лимит Slack?
Сразу верните 200 с пустым телом. Поставьте работу в шаг пайплайна. Для ответа в Slack используйте response_url из payload slash-команды внутри пайплайна.
Как защититься от replay-атак?
Сочетайте HMAC, метки времени провайдера (±5 минут) и ключи идемпотентности — запоздалые повторы отклоняются, а повторная доставка не даёт никакого эффекта.
Можно тестировать локально?
Stripe CLI stripe listen --forward-to или аналоги провайдера. Сохраняйте контракт event.body строкой, как шлюз отдаёт в проде.