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
- 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.
- 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.
- Verify the URL is built from non-empty IDs and the bot has VIEW_CHANNEL + SEND_MESSAGES on the target channel.
- 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
- Store the bot token in config once, validate shape at app start, and never pass empty IDs to request().
- Always read response.headers for Retry-After on 429 and back off accordingly.
- Log Discord's embedded errorText (it contains {code,message}) for precise diagnosis.
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
- Failed to get gateway URL: HTTP ${response.status} - ${error
- Failed to get access token: HTTP ${response.status}
- QQ API request failed ${endpoint}: HTTP ${response.status} -
- Discord bot token is required
- Rate limit exceeded
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/5d39a165068a88a0.
Report an issue: GitHub.