Inquir Compute · вебхуки

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 смешивает логику проверки и оси масштабирования, которые вы хотели развести.

Проверить, подтвердить, продолжить

Одна функция на провайдера держит проверку подписи маленькой, обозримой и независимой. Функция проверяет HMAC по сырому телу, записывает ключ идемпотентности против повторной обработки, сразу возвращает 200 и ставит тяжёлую работу в пайплайн.

Пайплайны работают в изолированных контейнерах с ретраями, трассами по шагам и общими секретами — у логики выполнения заказов Stripe та же наблюдаемость, что у HTTP API, а не чёрный ящик фонового воркера.

Возможности обработчика вебхуков

Проверка подписи на шлюзе (GitHub и Stripe)

Задайте webhookMode на маршруте — шлюз проверит HMAC по сырому телу до запуска функции. GitHub и Stripe встроены (для Stripe — с допуском по метке времени против replay), при несовпадении шлюз отвечает 403 BAD_SIGNATURE. Для других провайдеров body по-прежнему приходит строкой, и вы проверяете подписанные байты в обработчике.

Изолированные функции на провайдера

Сопоставьте каждому провайдеру свою функцию и маршрут. Логика Stripe не касается обработчиков GitHub; ревью безопасности сводится к одному небольшому файлу.

Передача в пайплайны

Быстро верните 200; выполнение заказа, письма или синхронизацию данных поставьте в шаг пайплайна, который работает вне HTTP-окна и не блокирует повторные доставки.

Идемпотентность и трассы выполнения

Записывайте ID событий до мутаций. Трассы в консоли показывают тело (с сокрытием чувствительного), заголовки, тайминги и число ретраев на каждую доставку.

Как собрать serverless-обработчик вебхуков на Inquir

Проверка по сырому телу, подтверждение до таймаута, идемпотентные записи.

1

Проверить подпись

Для GitHub и Stripe задайте webhookMode на маршруте — шлюз проверит сырое тело до запуска обработчика (403 при плохой подписи). Для других провайдеров читайте event.body как есть до любого JSON.parse и сравнивайте HMAC через timing-safe сравнение.

2

Записать ключ идемпотентности, вернуть 200

Сделайте upsert ID события провайдера до любой мутации состояния. Верните 200 в окне провайдера: Stripe ждёт < 30 с, Slack — < 3 с.

3

Поставить медленную работу в пайплайн

Для любой работы, которая не укладывается в окно таймаута, вызовите global.durable.startNew() и верните ссылку на задачу. Оркестрация ретраит независимо от HTTP-ответа.

Обработчики вебхуков Stripe и GitHub

Одна функция на провайдера. body приходит строкой — никогда не парсите до проверки. Используйте timing-safe сравнение, чтобы устоять против timing-атак на проверку HMAC.

webhooks/stripe.mjs
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' };
}
webhooks/github.mjs
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 строкой, который шлюз отдаёт в проде.