Inquir Compute logoInquir Compute
Сценарий · Inquir Compute

Slack webhooks и slash commands: ACK за 3 секунды и async response_url

Slack требует ответа за 3 секунды — иначе ретрай и сообщение «did not respond». Паттерн: ответить 200 сразу, отправить итог через `response_url` из async пайплайна.

Last updated: 2026-04-20

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 делает повтор.

Надёжный 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

1

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

HMAC с `SLACK_SIGNING_SECRET`; timestamp не старше 300 секунд.

2

Ответить 200 немедленно

Опционально: `{ "response_type": "in_channel", "text": "Processing..." }` — промежуточное сообщение.

3

Отправить результат через 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.

webhooks/slack.mjs (fast ACK handler)
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: '' };
}
jobs/slack-command.mjs (async work + response)
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, обработать событие в пайплайне.

Inquir Compute logoInquir Compute

Самый простой способ запускать AI-агентов и backend-джобы без инфраструктуры.

Связаться info@inquir.org

© 2025 Inquir Compute. Все права защищены.