CherryHQ/cherry-studio · error · Error

Slack API error ${method}: ${data.error ?? 'unknown error'}

Error message

Slack API error ${method}: ${data.error ?? 'unknown error'}

What it means

Thrown by SlackAdapter.apiRequest() when Slack returns HTTP 200 but the JSON body has ok:false. This is Slack's normal application-level error path — Slack almost always returns 200 even for logical errors and signals failure via the ok field and an error string. The data.error field carries a Slack-defined code like 'channel_not_found', 'invalid_auth', 'rate_limited', 'no_service', 'cannot_dm_bot'.

Source

Thrown at src/main/ai/channels/adapters/slack/SlackAdapter.ts:637

  private async apiRequest(method: string, body: Record<string, unknown>): Promise<unknown> {
    const response = await net.fetch(`${SLACK_API_BASE}/${method}`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.botToken}`,
        'Content-Type': 'application/json; charset=utf-8'
      },
      body: JSON.stringify(body)
    })

    if (!response.ok) {
      const errorText = await response.text().catch(() => '')
      throw new Error(`Slack API error ${method}: HTTP ${response.status} - ${errorText}`)
    }

    const data = (await response.json()) as { ok: boolean; error?: string }
    if (!data.ok) {
      throw new Error(`Slack API error ${method}: ${data.error ?? 'unknown error'}`)
    }

    return data
  }

  // ─── WebSocket Helper ──────────────────────────────────────

  private send(payload: object): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(payload))
    }
  }

  // ─── Lifecycle Helpers ──────────────────────────────────────

  private cleanup(): void {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)

View on GitHub (pinned to 726446b54c)

Solutions

  1. Invite the bot to the channel with /invite @botname — the most common cause of channel_not_found on send.
  2. If error is 'invalid_auth', verify the xoxb- token and re-install the app to the workspace to refresh scopes.
  3. For streaming chat.update failures, the SlackStreamingController already swallows flush errors (SlackAdapter.ts:185) — verify the catch chain is intact.
  4. For reactions errors, the addReaction/removeReaction helpers already catch and ignore (best-effort) — confirm no caller unwraps them.

Example fix

// before — flat throw, callers cannot branch on the Slack error code
if (!data.ok) {
  throw new Error(`Slack API error ${method}: ${data.error ?? 'unknown error'}`)
}

// after — typed error so callers can react to channel_not_found vs rate_limited
if (!data.ok) {
  throw new SlackApiError(method, data.error ?? 'unknown_error', { retryAfter: data.retryAfter })
}
// caller:
try { await adapter.sendMessage(chatId, text) }
catch (e) {
  if (e instanceof SlackApiError && e.code === 'channel_not_found') await inviteBot(chatId)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-validate token shape and channel membership to eliminate common ok:false causes.
const BOT_TOKEN_RE = /^xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+$/

if (!BOT_TOKEN_RE.test(config.bot_token)) {
  throw new ConfigError('Invalid Slack bot token format')
}
// Verify channel membership once at connect time via conversations.info:
// a non-member channel will later return channel_not_found on send.

Type guard

// Parse the Slack error code out of the thrown message for branching
function parseSlackApiError(e: unknown): { method: string; code: string } | null {
  const m = e instanceof Error
    ? e.message.match(/^Slack API error ([^:]+): ([a-z_]+|unknown error)/)
    : null
  return m ? { method: m[1], code: m[2] } : null
}

Try / catch

// Branch on the Slack error code for common recoverable failures
try {
  await adapter.sendMessage(chatId, text)
} catch (e) {
  const parsed = parseSlackApiError(e)
  if (parsed?.code === 'channel_not_found' || parsed?.code === 'not_in_channel') {
    await inviteBotToChannel(chatId) // /invite @botname
    await adapter.sendMessage(chatId, text) // retry once
  } else if (parsed?.code === 'rate_limited') {
    scheduleRetry(chatId, text, retryAfterMs)
  } else {
    throw e
  }
}

Prevention

When it happens

Trigger: chat.postMessage to a channel the bot is not a member of (error:'channel_not_found' or 'not_in_channel'); auth.test with a revoked bot token ('invalid_auth'); reactions.add on a message that was deleted ('bad_timestamp' or 'already_reacted'); chat.update with text exceeding limits or invalid formatting; rate_limited is normally a 429 but Slack occasionally surfaces it as ok:false. This path fires far more often than the HTTP-status path because Slack returns 200 for nearly everything.

Common situations: Bot was never invited to the target channel (channel_not_found) — the allowed_channel_ids config lists a channel ID the bot cannot post to; the bot was kicked from a channel after configuration; a streaming chat.update targets a ts that scrolled out of Slack's editable window; duplicate reaction add (already_reacted) on retry.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/40fb2df78f30c958. Report an issue: GitHub.