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
- Invite the bot to the channel with /invite @botname — the most common cause of channel_not_found on send.
- If error is 'invalid_auth', verify the xoxb- token and re-install the app to the workspace to refresh scopes.
- For streaming chat.update failures, the SlackStreamingController already swallows flush errors (SlackAdapter.ts:185) — verify the catch chain is intact.
- 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
- Invite the bot to all configured channels before sending — channel_not_found is the dominant cause.
- Pre-check membership via conversations.info at connect time for each allowed_channel_id.
- Wrap streaming chat.update in try/catch (the FlushController already swallows these) and verify that contract holds.
- Parse and log the Slack error code from the message — it is the primary diagnostic.
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
- Socket Mode connection failed: ${data.error ?? 'no URL retur
- Failed to open Socket Mode connection: HTTP ${response.statu
- Slack API error ${method}: HTTP ${response.status} - ${error
- Slack bot token (xoxb-...) is required
- Private key must be a non-empty string
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/40fb2df78f30c958.
Report an issue: GitHub.