Serverless-обработчик вебхуков с повторами, логами и фоновыми задачами
Паттерн: проверьте HMAC-подпись на шлюзе, ответьте 200 за 100 мс, передайте тяжёлую работу в пайплайн. Провайдер не делает повторную попытку; дублей не возникает.
Last updated: 2026-04-20
- HMAC signature verification on raw request body before any parsing
- Fast 200 ACK inside Stripe, GitHub, Slack, and Shopify timeout windows
- Async handoff to pipelines or jobs for slow downstream work
- Idempotency keys and execution traces for duplicate deliveries
Answer first
Direct answer
Serverless-обработчик вебхуков с повторами, логами и фоновыми задачами. Функция проверяет HMAC и сохраняет ключ идемпотентности, возвращает 200, вызывает `global.durable.startNew()`. Оркестрация продолжает работу вне HTTP-окна.
When it fits
- Stripe, GitHub, Slack или любой другой провайдер доставляет вебхуки
- Обработка занимает больше 2–5 секунд
Tradeoffs
- Синхронная обработка внутри HTTP-запроса — таймаут гарантирован при росте нагрузки или медленном downstream.
- Без ключа идемпотентности повторные доставки провайдера создают дубли в базе или дублируют вызовы API.
Нагрузка и где ломается
Почему вебхуки теряются или дублируются
Если обработчик отвечает 200 только после тяжёлой работы, тайм-аут провайдера вызывает повторную попытку — задача выполняется дважды.
Без проверки подписи любой запрос со знакомым URL может сымитировать вебхук и вызвать побочные эффекты.
Компромиссы
Где ломаются лёгкие подходы
Синхронная обработка внутри HTTP-запроса — таймаут гарантирован при росте нагрузки или медленном downstream.
Без ключа идемпотентности повторные доставки провайдера создают дубли в базе или дублируют вызовы API.
Как помогает Inquir
Как Inquir решает проблему
Функция проверяет HMAC и сохраняет ключ идемпотентности, возвращает 200, вызывает `global.durable.startNew()`. Оркестрация продолжает работу вне HTTP-окна.
Один шлюз принимает вебхуки Stripe, GitHub, Slack с раздельными маршрутами и API-ключами — без дополнительного nginx.
Что получаете
Что нужно надёжному обработчику вебхуков
Проверка подписи
HMAC SHA-256 timing-safe сравнение с секретом провайдера через переменные окружения функции.
Быстрый ACK
Ответить 200 до тяжёлой работы — иначе таймаут провайдера вызовет повтор.
Ключ идемпотентности
Проверить ID доставки перед записью; повторная доставка не создаёт дубль.
Запуск оркестрации
`global.durable.startNew()` запускает тяжёлую работу асинхронно с теми же логами и секретами.
Что делать дальше
Как построить обработчик вебхука
Verify on the raw body, ACK before the timeout, apply writes idempotently.
Проверить подпись
Сравните 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.
export async function handler(event) { const rawBody = event.body ?? ''; // Stripe signs the raw bytes — never parse before verifying const sig = event.headers['stripe-signature'] ?? ''; if (!stripe.webhooks.verifySignature(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET)) { return { statusCode: 400, body: 'invalid signature' }; } const evt = JSON.parse(rawBody); // Block duplicate delivery before any side effects const isNew = await db.upsertWebhookEvent(evt.id, evt.type); if (!isNew) return { statusCode: 200, body: 'duplicate' }; // Return fast; continue in pipeline 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 (!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 или любой другой провайдер доставляет вебхуки
- Обработка занимает больше 2–5 секунд
Когда лучше не трогать
- Лёгкий вебхук без side-effects, который отвечает за 100 мс — пайплайн избыточен
FAQ
Вопросы и ответы
Как проверить подпись Stripe?
Raw body с `transfer-encoding: identity`, HMAC SHA-256 с ключом из переменной окружения, timing-safe сравнение — полный код в примере страницы.
Что делать при дубле от провайдера?
Записать ID доставки в базу с `INSERT … ON CONFLICT DO NOTHING` до начала основной работы.
What about replay attacks?
Combine HMAC verification, provider-supplied timestamps (check within ±5 minutes), and idempotency keys so late replays are rejected and duplicate deliveries are no-ops.
Can I test locally?
Use the Stripe CLI `stripe listen --forward-to` or similar tools to forward real webhook events to a local handler. Keep the same event.body string contract as the gateway delivers in production.