Вебхуки и slash-команды Slack: ACK за 3 секунды и ответ через response_url
Slack требует ответа за 3 секунды — иначе ретраи и пометка приложения как медленного. Паттерн простой: ответить 200 сразу, а итоговый результат отправить через <code>response_url</code> из async-пайплайна.
Обновлено: 2026-06-28
Кратко
Суть ответа
Вебхуки и slash-команды Slack: ACK за 3 секунды и ответ через response_url. Функция получает raw body, проверяет X-Slack-Signature с временной меткой (против replay-атак), сохраняет response_url, отвечает 200 немедленно, запускает пайплайн.
Когда подходит и когда нет
- Slash command вызывает БД, LLM или внешний API
- Ответ занимает больше 2 секунд
На что обратить внимание
- Верификация Slack-подписи после парсинга тела: Slack подписывает raw body с timestamp; JSON.parse меняет порядок ключей — подпись не совпадает.
- Синхронный запрос к downstream API в HTTP-обработчике: 3 секунды уходят, Slack делает повтор.
Ситуация: нагрузка и где обычно ломается
Почему slash-команды Slack теряются
- Slash-команды: HTTP 200 за 3 с — иначе приложение помечают как «не ответило»
- Event subscriptions: ACK за 3 с, обработка может продолжаться асинхронно
- Interactive components (кнопки, модалки): то же 3-секундное окно
Большинство операций Slack — LLM, БД, внешние API — занимают дольше 3 с. Стандартный паттерн: ACK сразу, работа в пайплайне, результат через response_url или Web API.
Когда простых рецептов недостаточно
Типичные ошибки со slash-командами и Events API
Верификация 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-обработчика
Верификация подписи
HMAC SHA-256 по v0:${timestamp}:${rawBody}; timestamp не старше 300 с — защита от replay до timing-safe сравнения с X-Slack-Signature.
3-секундный ACK
Ответить 200 до любой тяжёлой работы: Slack не делает ретрай, пользователь не видит ошибку.
response_url паттерн
Сохранить response_url из тела; пайплайн отправляет результат через fetch(response_url, { method: "POST" }).
Маршрутизация slash-команд
Разные команды — разные pipeline jobs по полю command.
Дедупликация ретраев
Slack может повторить slash-команду или Events API. Ключ идемпотентности — trigger_id или event_id — до global.durable.startNew.
Что сделать дальше, по шагам
Как обработать 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 (expected.length !== received.length || !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 секунд
Когда лучше выбрать другое
- Простая команда с мгновенным статическим ответом
Вопросы и ответы
Вопросы и ответы
Как проверить подпись Slack?
Строка v0:${timestamp}:${rawBody} → HMAC-SHA256 с signing secret → timing-safe сравнение с полным значением X-Slack-Signature (оба с префиксом v0=).
Slack Events API или Slash Commands?
Тот же паттерн: проверить X-Slack-Signature, ответить 200, обработать событие в пайплайне.
Что если Slack повторит slash-команду?
Используйте trigger_id (slash commands) или event_id (Events API) как ключ идемпотентности — upsert до global.durable.startNew, чтобы ретрай вернул 200 без второго запуска пайплайна.