Inquir Compute · вебхуки

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.

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

Функция проверяет HMAC и сохраняет ключ идемпотентности, возвращает 200, вызывает global.durable.startNew(). Оркестрация продолжает работу вне HTTP-окна.

Один шлюз обслуживает вебхуки Stripe, GitHub, Slack с раздельными маршрутами и API-ключами — без дополнительного nginx.

Что нужно надёжному обработчику вебхуков

Проверка подписи

Timing-safe сравнение HMAC SHA-256 с секретом провайдера из переменных окружения функции.

Быстрый ACK

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

Ключ идемпотентности

Проверить ID доставки перед записью; повторная доставка не создаёт дубль.

Запуск оркестрации

global.durable.startNew() запускает тяжёлую работу асинхронно с теми же логами и секретами.

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

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

1

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

Сравните HMAC из заголовка с вычисленным значением — до любой другой работы.

2

Ответить 200 быстро

Запишите ключ идемпотентности и вызовите global.durable.startNew() до возврата ответа.

3

Обработать событие в пайплайне

Шаги пайплайна выполняют тяжёлую работу: 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.

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 — вы не трогаете 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 строкой, как шлюз отдаёт в проде.