CherryHQ/cherry-studio · error · Error

Discord API error ${url}: HTTP ${response.status} - ${errorT

Error message

Discord API error ${url}: HTTP ${response.status} - ${errorText}

What it means

Thrown by DiscordAdapter's request wrapper after Electron's net.fetch to a Discord REST endpoint returns a non-2xx status (response.ok is false). The message embeds the target URL, the HTTP status, and the raw error body that Discord returned. It is a generic gateway over every Discord HTTP call (gateway lookup, interaction callback, channel/message POSTs).

Source

Thrown at src/main/ai/channels/adapters/discord/DiscordAdapter.ts:761

  // ─── REST API Helper ─────────────────────────────────────────

  private async apiRequest(
    url: string,
    options?: { method?: string; body?: Record<string, unknown> | Record<string, unknown>[] }
  ): Promise<Response> {
    const response = await net.fetch(url, {
      method: options?.method ?? 'GET',
      headers: {
        Authorization: `Bot ${this.botToken}`,
        'Content-Type': 'application/json',
        'User-Agent': USER_AGENT
      },
      ...(options?.body ? { body: JSON.stringify(options.body) } : {})
    })

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

    return response
  }

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

  private cleanup(): void {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
      this.reconnectTimer = null
    }
    if (this.heartbeatJitterTimer) {
      clearTimeout(this.heartbeatJitterTimer)
      this.heartbeatJitterTimer = null
    }
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)

View on GitHub (pinned to 726446b54c)

Solutions

  1. Inspect the embedded HTTP status: 401/403 -> regenerate the bot token in the Discord Developer Portal and re-add the bot with correct scopes (bot + applications.commands); 429 -> implement exponential backoff honoring Discord's Retry-After / X-RateLimit headers; 5xx -> surface a transient error and retry.
  2. Log the full errorText from the message to read Discord's {code, message} JSON (e.g. code 50001 Missing Access), which pinpoints the exact permission gap.
  3. Verify the URL is built from non-empty IDs and the bot has VIEW_CHANNEL + SEND_MESSAGES on the target channel.
  4. Confirm USER_AGENT and Authorization: Bot <token> headers are present (they always are here), so the failure is server-side, not header omission.

Example fix

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

// after — honor rate limits and surface Discord's code/message
if (!response.ok) {
  const errorText = await response.text().catch(() => '')
  if (response.status === 429) {
    const retryAfter = Number(response.headers.get('Retry-After') ?? '1')
    await new Promise((r) => setTimeout(r, retryAfter * 1000))
    return this.request(url, options) // one bounded retry
  }
  throw new Error(`Discord API error ${url}: HTTP ${response.status} - ${errorText}`)
}
Defensive patterns

Strategy: retry

Validate before calling

// Before calling DiscordAdapter, confirm the bot token shape and target IDs are non-empty.
function assertDiscordReady(botToken: unknown, targetId: unknown) {
  if (typeof botToken !== 'string' || !botToken.startsWith('Bot ') && !botToken.trim()) {
    throw new Error('Discord bot token missing')
  }
  if (typeof targetId !== 'string' || !/^\d{17,20}$/.test(targetId)) {
    throw new Error('Discord target id must be a snowflake')
  }
}

Type guard

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

Try / catch

try {
  await adapter.sendToChannel(channelId, text)
} catch (e) {
  if (isDiscordHttpError(e)) {
    const status = Number(/HTTP (\d+)/.exec(e.message)?.[1] ?? 0)
    if (status === 429) return backoffAndRetry(() => adapter.sendToChannel(channelId, text))
    if (status >= 500) return transientRetry()
  }
  throw e
}

Prevention

When it happens

Trigger: Any Discord REST call inside DiscordAdapter (e.g. GET /gateway/bot at line 272, POST interactions callback at lines 663/674, or the shared request() at line ~749) whose response.status is outside 200-299. The body is read via response.text() with a .catch fallback to '' so the throw always fires for non-ok responses.

Common situations: Bot token revoked or mis-pasted (401 Unauthorized), missing scopes/intents (403 Forbidden), rate limited (429 with Retry-After), attempting to message a channel the bot can't see, Discord incident returning 5xx, or a malformed endpoint URL built from a missing guild/channel id.

Related errors


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