{"record":{"id":"5d39a165068a88a0","repo":"CherryHQ/cherry-studio","slug":"discord-api-error-url-http-response-status","errorCode":null,"errorMessage":"Discord API error ${url}: HTTP ${response.status} - ${errorText}","messagePattern":"Discord API error (.+?): HTTP (.+?) - (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/discord/DiscordAdapter.ts","lineNumber":761,"sourceCode":"  // ─── REST API Helper ─────────────────────────────────────────\n\n  private async apiRequest(\n    url: string,\n    options?: { method?: string; body?: Record<string, unknown> | Record<string, unknown>[] }\n  ): Promise<Response> {\n    const response = await net.fetch(url, {\n      method: options?.method ?? 'GET',\n      headers: {\n        Authorization: `Bot ${this.botToken}`,\n        'Content-Type': 'application/json',\n        'User-Agent': USER_AGENT\n      },\n      ...(options?.body ? { body: JSON.stringify(options.body) } : {})\n    })\n\n    if (!response.ok) {\n      const errorText = await response.text().catch(() => '')\n      throw new Error(`Discord API error ${url}: HTTP ${response.status} - ${errorText}`)\n    }\n\n    return response\n  }\n\n  // ─── Lifecycle Helpers ────────────────────────────────────────\n\n  private cleanup(): void {\n    if (this.reconnectTimer) {\n      clearTimeout(this.reconnectTimer)\n      this.reconnectTimer = null\n    }\n    if (this.heartbeatJitterTimer) {\n      clearTimeout(this.heartbeatJitterTimer)\n      this.heartbeatJitterTimer = null\n    }\n    if (this.heartbeatTimer) {\n      clearInterval(this.heartbeatTimer)","sourceCodeStart":743,"sourceCodeEnd":779,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/discord/DiscordAdapter.ts#L743-L779","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (!response.ok) {\n  const errorText = await response.text().catch(() => '')\n  throw new Error(`Discord API error ${url}: HTTP ${response.status} - ${errorText}`)\n}\n\n// after — honor rate limits and surface Discord's code/message\nif (!response.ok) {\n  const errorText = await response.text().catch(() => '')\n  if (response.status === 429) {\n    const retryAfter = Number(response.headers.get('Retry-After') ?? '1')\n    await new Promise((r) => setTimeout(r, retryAfter * 1000))\n    return this.request(url, options) // one bounded retry\n  }\n  throw new Error(`Discord API error ${url}: HTTP ${response.status} - ${errorText}`)\n}","handlingStrategy":"retry","validationCode":"// Before calling DiscordAdapter, confirm the bot token shape and target IDs are non-empty.\nfunction assertDiscordReady(botToken: unknown, targetId: unknown) {\n  if (typeof botToken !== 'string' || !botToken.startsWith('Bot ') && !botToken.trim()) {\n    throw new Error('Discord bot token missing')\n  }\n  if (typeof targetId !== 'string' || !/^\\d{17,20}$/.test(targetId)) {\n    throw new Error('Discord target id must be a snowflake')\n  }\n}","typeGuard":"function isDiscordHttpError(e: unknown): e is Error {\n  return e instanceof Error && /^Discord API error .*: HTTP \\d+/.test(e.message)\n}","tryCatchPattern":"try {\n  await adapter.sendToChannel(channelId, text)\n} catch (e) {\n  if (isDiscordHttpError(e)) {\n    const status = Number(/HTTP (\\d+)/.exec(e.message)?.[1] ?? 0)\n    if (status === 429) return backoffAndRetry(() => adapter.sendToChannel(channelId, text))\n    if (status >= 500) return transientRetry()\n  }\n  throw e\n}","preventionTips":["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."],"tags":["discord","http","rate-limit","auth","network"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}