Serverless-обработка вебхуков внешних провайдеров
Stripe, GitHub, Slack, Shopify: проверяйте подпись по сырому телу запроса, отвечайте до таймаута провайдера, выносите медленную работу в async-пайплайны, закладывайте идемпотентность на случай повторных доставок и сохраняйте трассировки, когда ретраи накладываются друг на друга.
Обновлено: 2026-06-28
Кратко
Суть ответа
Serverless-обработка вебхуков внешних провайдеров. Отдельные функции под каждого провайдера снижают случайное связывание между обработчиками вебхуков.
Когда подходит и когда нет
- Интеграции SaaS
- Платежи
- События с Git-хостинга
На что обратить внимание
- Пропуск проверки подписи «временно» оставляет эндпоинт открытым для поддельных запросов — задолго после того, как об этом упрощении забыли.
- Синхронные записи в БД до ответа 200 OK провоцируют ретраи провайдера — и дубли записей — при любом всплеске задержки ниже по стеку.
Ситуация: нагрузка и где обычно ломается
Типичные сбои при обработке вебхуков
Дубликаты доставок и медленные вызовы вниз по стеку заканчиваются таймаутами, если не заложить идемпотентность и передачу тяжёлой работы в фон.
Одна перегруженная точка входа тянет вниз не связанный с ней пользовательский трафик.
One overloaded endpoint drags down unrelated user traffic.
Когда простых рецептов недостаточно
Опасные упрощения
Пропуск проверки подписи «временно» оставляет эндпоинт открытым для поддельных запросов — задолго после того, как об этом упрощении забыли.
Синхронные записи в БД до ответа 200 OK провоцируют ретраи провайдера — и дубли записей — при любом всплеске задержки ниже по стеку.
Как Inquir помогает в этом сценарии
Архитектура вебхука: проверить, быстро ответить, продолжить асинхронно
Отдельные функции под каждого провайдера снижают случайное связывание между обработчиками вебхуков.
Пайплайны продолжают работу после быстрого HTTP-ответа, поэтому таймауты провайдера никогда не блокируют идемпотентные записи.
Что вы получаете на платформе
API-шлюз для вебхуков: аутентификация, трассы, тенанты
Доступ на уровне маршрута
API-ключи или собственные схемы аутентификации на каждый входящий маршрут — ещё до кода хендлера.
Трассы
Видно, на каком шаге случился сбой — проверка подписи, постановка в очередь или запись данных.
Маршрутизация тенантов
Разводите хосты или префиксы путей, когда у каждого клиента свой URL вебхука.
Что сделать дальше, по шагам
Как обрабатывать вебхуки на Inquir Compute
Проверка подписи, быстрый ответ провайдеру, идемпотентные записи.
Проверить событие
Отклоняйте неверные подписи и блокируйте дубликаты до любых изменений состояния.
Ответить
Подтверждайте получение в пределах таймаута провайдера, чтобы он не ретраил без необходимости.
Обработать
Продолжайте тяжёлую работу асинхронно и держите записи идемпотентными по ID события провайдера.
Пример кода
Паттерны реализации под конкретного провайдера
У разных провайдеров разные заголовки подписи и таймауты. Отдельная функция на провайдера: логика проверки остаётся маленькой и легко проверяемой.
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' }; }
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 события у провайдера ключом идемпотентности: фиксируйте «уже видели» до изменения данных — тогда доставка «как минимум один раз» остаётся безопасной.