Сценарий · Inquir Compute

Вебхуки и 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 делает повтор.

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

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 (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: '' };
}
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, обработать событие в пайплайне.

Что если Slack повторит slash-команду?

Используйте trigger_id (slash commands) или event_id (Events API) как ключ идемпотентности — upsert до global.durable.startNew, чтобы ретрай вернул 200 без второго запуска пайплайна.