{"record":{"id":"40fb2df78f30c958","repo":"CherryHQ/cherry-studio","slug":"slack-api-error-method-data-error-unknow","errorCode":null,"errorMessage":"Slack API error ${method}: ${data.error ?? 'unknown error'}","messagePattern":"Slack API error (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/slack/SlackAdapter.ts","lineNumber":637,"sourceCode":"\n  private async apiRequest(method: string, body: Record<string, unknown>): Promise<unknown> {\n    const response = await net.fetch(`${SLACK_API_BASE}/${method}`, {\n      method: 'POST',\n      headers: {\n        Authorization: `Bearer ${this.botToken}`,\n        'Content-Type': 'application/json; charset=utf-8'\n      },\n      body: JSON.stringify(body)\n    })\n\n    if (!response.ok) {\n      const errorText = await response.text().catch(() => '')\n      throw new Error(`Slack API error ${method}: HTTP ${response.status} - ${errorText}`)\n    }\n\n    const data = (await response.json()) as { ok: boolean; error?: string }\n    if (!data.ok) {\n      throw new Error(`Slack API error ${method}: ${data.error ?? 'unknown error'}`)\n    }\n\n    return data\n  }\n\n  // ─── WebSocket Helper ──────────────────────────────────────\n\n  private send(payload: object): void {\n    if (this.ws?.readyState === WebSocket.OPEN) {\n      this.ws.send(JSON.stringify(payload))\n    }\n  }\n\n  // ─── Lifecycle Helpers ──────────────────────────────────────\n\n  private cleanup(): void {\n    if (this.reconnectTimer) {\n      clearTimeout(this.reconnectTimer)","sourceCodeStart":619,"sourceCodeEnd":655,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/slack/SlackAdapter.ts#L619-L655","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — flat throw, callers cannot branch on the Slack error code\nif (!data.ok) {\n  throw new Error(`Slack API error ${method}: ${data.error ?? 'unknown error'}`)\n}\n\n// after — typed error so callers can react to channel_not_found vs rate_limited\nif (!data.ok) {\n  throw new SlackApiError(method, data.error ?? 'unknown_error', { retryAfter: data.retryAfter })\n}\n// caller:\ntry { await adapter.sendMessage(chatId, text) }\ncatch (e) {\n  if (e instanceof SlackApiError && e.code === 'channel_not_found') await inviteBot(chatId)\n}","handlingStrategy":"try-catch","validationCode":"// Pre-validate token shape and channel membership to eliminate common ok:false causes.\nconst BOT_TOKEN_RE = /^xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+$/\n\nif (!BOT_TOKEN_RE.test(config.bot_token)) {\n  throw new ConfigError('Invalid Slack bot token format')\n}\n// Verify channel membership once at connect time via conversations.info:\n// a non-member channel will later return channel_not_found on send.","typeGuard":"// Parse the Slack error code out of the thrown message for branching\nfunction parseSlackApiError(e: unknown): { method: string; code: string } | null {\n  const m = e instanceof Error\n    ? e.message.match(/^Slack API error ([^:]+): ([a-z_]+|unknown error)/)\n    : null\n  return m ? { method: m[1], code: m[2] } : null\n}","tryCatchPattern":"// Branch on the Slack error code for common recoverable failures\ntry {\n  await adapter.sendMessage(chatId, text)\n} catch (e) {\n  const parsed = parseSlackApiError(e)\n  if (parsed?.code === 'channel_not_found' || parsed?.code === 'not_in_channel') {\n    await inviteBotToChannel(chatId) // /invite @botname\n    await adapter.sendMessage(chatId, text) // retry once\n  } else if (parsed?.code === 'rate_limited') {\n    scheduleRetry(chatId, text, retryAfterMs)\n  } else {\n    throw e\n  }\n}","preventionTips":["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."],"tags":["slack","web-api","api-error","config","authentication"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}