{"record":{"id":"c7f1f81a640cdcff","repo":"CherryHQ/cherry-studio","slug":"slack-api-error-method-http-response-status","errorCode":null,"errorMessage":"Slack API error ${method}: HTTP ${response.status} - ${errorText}","messagePattern":"Slack API error (.+?): HTTP (.+?) - (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/slack/SlackAdapter.ts","lineNumber":632,"sourceCode":"      // Best-effort removal\n    }\n  }\n\n  // ─── Slack Web API Helper ──────────────────────────────────\n\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","sourceCodeStart":614,"sourceCodeEnd":650,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/slack/SlackAdapter.ts#L614-L650","documentation":"Thrown by SlackAdapter.apiRequest() when a POST to any Slack Web API method (chat.postMessage, chat.update, auth.test, reactions.add, users.info) returns a non-2xx HTTP status. This is the transport-level branch: Slack did not return a 200 OK. The errorText is the raw response body (best-effort, may be empty). Method names are the Slack Web API method path segment (e.g. 'chat.postMessage').","triggerScenarios":"Any apiRequest() call where Slack returns 4xx/5xx: 429 means rate-limited (Retry-After header should be honored), 401 means the bot token (xoxb-) is invalid, 403 means missing scopes, 404 means the method does not exist, 5xx is a Slack incident. The bot token is the credential here, not the app token. This helper is used by sendMessage, onTextUpdate streaming, reactions, users.info, and auth.test.","commonSituations":"Bursting chat.update calls faster than the ~1/s per-channel Slack limit (the SLACK_STREAM_THROTTLE_MS=1500 constant exists for this) and hitting 429; the bot token was revoked when the app was uninstalled from the workspace; the bot lacks chat:write scope to post in a particular channel; Slack API outage returns 503.","solutions":["For HTTP 429: implement Retry-After header respect and back off — the existing 100ms inter-chunk delay in sendMessage is too short under heavy load.","For HTTP 401/403: verify the xoxb- bot token is valid and has the required scopes (chat:write, reactions:write, users:read, files:read).","For HTTP 5xx: check status.slack.com and let the existing scheduleReconnect()/catch handlers retry.","Capture response.headers.get('retry-after') before reading the body when status===429."],"exampleFix":"// before — no retry on rate limit, body text may be empty\nif (!response.ok) {\n  const errorText = await response.text().catch(() => '')\n  throw new Error(`Slack API error ${method}: HTTP ${response.status} - ${errorText}`)\n}\n\n// after — honor Retry-After for 429 so callers can back off\nif (response.status === 429) {\n  const retryAfter = Number(response.headers.get('retry-after') ?? '1')\n  throw new SlackRateLimitError(method, retryAfter)\n}\nif (!response.ok) {\n  const errorText = await response.text().catch(() => '')\n  throw new Error(`Slack API error ${method}: HTTP ${response.status} - ${errorText}`)\n}","handlingStrategy":"retry","validationCode":"// Pre-check the bot token shape to eliminate the most common transport errors (401).\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('Slack bot token must be xoxb-... format')\n}\n// Also verify the bot is a member of the target channels before sending:\n// (call conversations.list / conversations.info to confirm membership up-front)","typeGuard":"function isSlackHttpError(e: unknown): e is Error {\n  return e instanceof Error && /Slack API error .*: HTTP \\d+/.test(e.message)\n}\n\nfunction extractHttpStatus(e: unknown): number | null {\n  const m = e instanceof Error ? e.message.match(/HTTP (\\d+)/) : null\n  return m ? Number(m[1]) : null\n}","tryCatchPattern":"// For 429 specifically, honor Retry-After (requires a richer error — see exampleFix).\n// With the current plain-Error shape, wrap sends in a bounded retry on 5xx/429:\nasync function sendWithRetry(adapter, chatId, text, attempts = 3) {\n  for (let i = 0; i < attempts; i++) {\n    try {\n      return await adapter.sendMessage(chatId, text)\n    } catch (e) {\n      const status = extractHttpStatus(e)\n      if (status && (status === 429 || status >= 500) && i < attempts - 1) {\n        await new Promise((r) => setTimeout(r, 1000 * (i + 1)))\n        continue\n      }\n      throw e\n    }\n  }\n}","preventionTips":["Throttle chat.update below 1/s per channel — SLACK_STREAM_THROTTLE_MS=1500 already does this; do not reduce it.","Invite the bot (@botname) to every channel in allowed_channel_ids before enabling notifications.","Verify the xoxb- token format at config-write time.","Check status.slack.com before debugging persistent 5xx — it is often an incident."],"tags":["slack","web-api","network","rate-limiting","api-error"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}