Slack webhooks и slash commands: ACK за 3 секунды и async response_url
Slack требует ответа за 3 секунды — иначе ретрай и сообщение «did not respond». Паттерн: ответить 200 сразу, отправить итог через `response_url` из async пайплайна.
Last updated: 2026-04-20
Answer first
Direct answer
Slack webhooks и slash commands: ACK за 3 секунды и async response_url. Функция получает raw body, проверяет `X-Slack-Signature` с временной меткой (против replay-атак), сохраняет `response_url`, отвечает 200 немедленно, запускает пайплайн.
When it fits
- Slash command вызывает БД, LLM или внешний API
- Ответ занимает больше 2 секунд
Tradeoffs
- Верификация Slack-подписи после парсинга тела: Slack подписывает raw body с timestamp; JSON.parse меняет порядок ключей — подпись не совпадает.
- Синхронный запрос к downstream API в HTTP-обработчике: 3 секунды уходят, Slack делает повтор.
Нагрузка и где ломается
Почему Slack slash commands теряются
- Slash commands: Slack requires HTTP 200 within 3 seconds or marks app as unresponsive
- Event subscriptions: acknowledgement required within 3 seconds, processing can continue
- Interactive components (buttons, modals): same 3-second window
Slack ждёт ответа 3 секунды. Если функция делает запрос к БД или API до ответа — пользователь видит «did not respond» или задвоенный запрос.
Где костыли не спасают
Типичные ошибки
Верификация Slack-подписи после парсинга тела: Slack подписывает raw body с timestamp; JSON.parse меняет порядок ключей — подпись не совпадает.
Синхронный запрос к downstream API в HTTP-обработчике: 3 секунды уходят, Slack делает повтор.
Как помогает Inquir
Надёжный Slack webhook на Inquir
Функция получает raw body, проверяет `X-Slack-Signature` с временной меткой (против replay-атак), сохраняет `response_url`, отвечает 200 немедленно, запускает пайплайн.
Пайплайн делает тяжёлую работу и отправляет итог через `response_url`. Пользователь видит ответ — не «did not respond».
Что получаете
Ключевые части Slack webhook-обработчика
3-секундный ACK
Ответить 200 до любой тяжёлой работы: Slack не делает ретрай, пользователь не видит ошибку.
Верификация подписи
HMAC SHA-256 с `X-Slack-Signature` и `X-Slack-Request-Timestamp` — защита от replay-атак.
response_url паттерн
Сохранить `response_url` из тела; пайплайн отправляет результат через `fetch(response_url, { method: "POST" })`.
Защита от replay
Проверить, что `timestamp` не старше 5 минут до HMAC-сравнения.
Что делать дальше
Как обработать Slack slash command
Проверить подпись и timestamp
HMAC с `SLACK_SIGNING_SECRET`; timestamp не старше 300 секунд.
Ответить 200 немедленно
Опционально: `{ "response_type": "in_channel", "text": "Processing..." }` — промежуточное сообщение.
Отправить результат через response_url
Пайплайн делает работу и `POST response_url` с финальным ответом.
Пример кода
Slack slash command handler
Verify, ACK, trigger async pipeline—all within the 3-second window. The pipeline step does the work and posts back via response_url.
import { createHmac, timingSafeEqual } from 'node:crypto'; export async function handler(event) { const body = event.body ?? ''; const timestamp = event.headers['x-slack-request-timestamp'] ?? ''; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return { statusCode: 400, body: 'stale request' }; } const sigBase = `v0:${timestamp}:${body}`; const expected = 'v0=' + createHmac('sha256', process.env.SLACK_SIGNING_SECRET).update(sigBase).digest('hex'); const received = event.headers['x-slack-signature'] ?? ''; if (!timingSafeEqual(Buffer.from(expected), Buffer.from(received))) { return { statusCode: 401, body: 'invalid signature' }; } const params = Object.fromEntries(new URLSearchParams(body)); await global.durable.startNew('slack-command', undefined, { command: params.command, text: params.text, userId: params.user_id, channelId: params.channel_id, responseUrl: params.response_url, }); return { statusCode: 200, body: '' }; }
export async function handler(event) { const { command, text, userId, responseUrl } = event.payload ?? {}; const result = await processCommand(command, text, userId); await fetch(responseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ response_type: 'in_channel', blocks: [{ type: 'section', text: { type: 'mrkdwn', text: result } }], }), }); return { command, userId, result }; }
Когда подходит
Когда нужен async паттерн для Slack
Когда это уместно
- Slash command вызывает БД, LLM или внешний API
- Ответ занимает больше 2 секунд
Когда лучше не трогать
- Простая команда с мгновенным статическим ответом
FAQ
Вопросы и ответы
Как проверить подпись Slack?
`v0=${timestamp}:${rawBody}` → HMAC-SHA256 с signing secret → timing-safe сравнение с `X-Slack-Signature` (без `v0=` префикса).
Slack Events API или Slash Commands?
Тот же паттерн: проверить `X-Slack-Signature`, ответить 200, обработать событие в пайплайне.