Сценарий

Serverless-обработка вебхуков внешних провайдеров

Stripe, GitHub, Slack, Shopify: проверяйте подпись по сырому телу запроса, отвечайте до таймаута провайдера, выносите медленную работу в async-пайплайны, закладывайте идемпотентность на случай повторных доставок и сохраняйте трассировки, когда ретраи накладываются друг на друга.

Обновлено: 2026-06-28

Суть ответа

Serverless-обработка вебхуков внешних провайдеров. Отдельные функции под каждого провайдера снижают случайное связывание между обработчиками вебхуков.

Когда подходит и когда нет

  • Интеграции SaaS
  • Платежи
  • События с Git-хостинга

На что обратить внимание

  • Пропуск проверки подписи «временно» оставляет эндпоинт открытым для поддельных запросов — задолго после того, как об этом упрощении забыли.
  • Синхронные записи в БД до ответа 200 OK провоцируют ретраи провайдера — и дубли записей — при любом всплеске задержки ниже по стеку.

Типичные сбои при обработке вебхуков

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

Одна перегруженная точка входа тянет вниз не связанный с ней пользовательский трафик.

One overloaded endpoint drags down unrelated user traffic.

Опасные упрощения

Пропуск проверки подписи «временно» оставляет эндпоинт открытым для поддельных запросов — задолго после того, как об этом упрощении забыли.

Синхронные записи в БД до ответа 200 OK провоцируют ретраи провайдера — и дубли записей — при любом всплеске задержки ниже по стеку.

Архитектура вебхука: проверить, быстро ответить, продолжить асинхронно

Отдельные функции под каждого провайдера снижают случайное связывание между обработчиками вебхуков.

Пайплайны продолжают работу после быстрого HTTP-ответа, поэтому таймауты провайдера никогда не блокируют идемпотентные записи.

API-шлюз для вебхуков: аутентификация, трассы, тенанты

Доступ на уровне маршрута

API-ключи или собственные схемы аутентификации на каждый входящий маршрут — ещё до кода хендлера.

Трассы

Видно, на каком шаге случился сбой — проверка подписи, постановка в очередь или запись данных.

Маршрутизация тенантов

Разводите хосты или префиксы путей, когда у каждого клиента свой URL вебхука.

Как обрабатывать вебхуки на Inquir Compute

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

1

Проверить событие

Отклоняйте неверные подписи и блокируйте дубликаты до любых изменений состояния.

2

Ответить

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

3

Обработать

Продолжайте тяжёлую работу асинхронно и держите записи идемпотентными по ID события провайдера.

Паттерны реализации под конкретного провайдера

У разных провайдеров разные заголовки подписи и таймауты. Отдельная функция на провайдера: логика проверки остаётся маленькой и легко проверяемой.

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('process-payment', undefined, { eventId: evt.id, type: evt.type, obj: evt.data.object });
  return { statusCode: 200, body: 'ok' };
}
webhooks/shopify.mjs
import { createHmac } from 'node:crypto';

export async function handler(event) {
  const rawBody = event.body ?? '';
  const hmac = event.headers['x-shopify-hmac-sha256'];
  const digest = createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET).update(rawBody).digest('base64');
  if (hmac !== digest) return { statusCode: 401, body: 'invalid signature' };
  const topic = event.headers['x-shopify-topic']; // e.g. 'orders/create'
  const order = JSON.parse(rawBody);
  await global.durable.startNew('process-order', undefined, { topic, orderId: order.id });
  return { statusCode: 200, body: '' };
}

Когда этот сценарий подходит

Когда это уместно

  • Интеграции SaaS
  • Платежи
  • События с Git-хостинга

Когда лучше выбрать другое

  • Двунаправленные сокеты — другой класс транспорта

Вопросы и ответы

Почему сырое тело нельзя трогать до проверки HMAC?

Провайдеры подписывают точные байты; пересериализация JSON или смена кодировки меняет payload и даёт ложные ошибки «invalid signature» на валидных событиях.

Можно ли делать тяжёлую работу до ответа 200?

Нет — подтвердите получение в пределах таймаута провайдера, а работу продолжите в пайплайне или фоновой задаче, чтобы ретраи не дублировали дорогие побочные эффекты.

Как обрабатывать дубликаты доставки?

Считайте ID события у провайдера ключом идемпотентности: фиксируйте «уже видели» до изменения данных — тогда доставка «как минимум один раз» остаётся безопасной.