CherryHQ/cherry-studio · error · Error

Slack API error ${method}: HTTP ${response.status} - ${error

Error message

Slack API error ${method}: HTTP ${response.status} - ${errorText}

What it means

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').

Source

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

      // Best-effort removal
    }
  }

  // ─── Slack Web API Helper ──────────────────────────────────

  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))
    }
  }

View on GitHub (pinned to 726446b54c)

Solutions

  1. 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.
  2. For HTTP 401/403: verify the xoxb- bot token is valid and has the required scopes (chat:write, reactions:write, users:read, files:read).
  3. For HTTP 5xx: check status.slack.com and let the existing scheduleReconnect()/catch handlers retry.
  4. Capture response.headers.get('retry-after') before reading the body when status===429.

Example fix

// before — no retry on rate limit, body text may be empty
if (!response.ok) {
  const errorText = await response.text().catch(() => '')
  throw new Error(`Slack API error ${method}: HTTP ${response.status} - ${errorText}`)
}

// after — honor Retry-After for 429 so callers can back off
if (response.status === 429) {
  const retryAfter = Number(response.headers.get('retry-after') ?? '1')
  throw new SlackRateLimitError(method, retryAfter)
}
if (!response.ok) {
  const errorText = await response.text().catch(() => '')
  throw new Error(`Slack API error ${method}: HTTP ${response.status} - ${errorText}`)
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-check the bot token shape to eliminate the most common transport errors (401).
const BOT_TOKEN_RE = /^xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+$/

if (!BOT_TOKEN_RE.test(config.bot_token)) {
  throw new ConfigError('Slack bot token must be xoxb-... format')
}
// Also verify the bot is a member of the target channels before sending:
// (call conversations.list / conversations.info to confirm membership up-front)

Type guard

function isSlackHttpError(e: unknown): e is Error {
  return e instanceof Error && /Slack API error .*: HTTP \d+/.test(e.message)
}

function extractHttpStatus(e: unknown): number | null {
  const m = e instanceof Error ? e.message.match(/HTTP (\d+)/) : null
  return m ? Number(m[1]) : null
}

Try / catch

// For 429 specifically, honor Retry-After (requires a richer error — see exampleFix).
// With the current plain-Error shape, wrap sends in a bounded retry on 5xx/429:
async function sendWithRetry(adapter, chatId, text, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await adapter.sendMessage(chatId, text)
    } catch (e) {
      const status = extractHttpStatus(e)
      if (status && (status === 429 || status >= 500) && i < attempts - 1) {
        await new Promise((r) => setTimeout(r, 1000 * (i + 1)))
        continue
      }
      throw e
    }
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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