{"record":{"id":"2ee7f2f72052ac91","repo":"CherryHQ/cherry-studio","slug":"qq-api-request-failed-endpoint-http-response","errorCode":null,"errorMessage":"QQ API request failed ${endpoint}: HTTP ${response.status} - ${errorText}","messagePattern":"QQ API request failed (.+?): HTTP (.+?) - (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/qq/QqAdapter.ts","lineNumber":205,"sourceCode":"\n  private async apiRequest(\n    endpoint: string,\n    options?: { method?: string; body?: Record<string, unknown> }\n  ): Promise<Response> {\n    const token = await this.getAccessToken()\n    const response = await net.fetch(endpoint, {\n      method: options?.method ?? 'GET',\n      headers: {\n        Authorization: `QQBot ${token}`,\n        'Content-Type': 'application/json',\n        'X-Union-Appid': this.appId\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(`QQ API request failed ${endpoint}: HTTP ${response.status} - ${errorText}`)\n    }\n\n    return response\n  }\n\n  private async getGatewayUrl(): Promise<string> {\n    const response = await this.apiRequest(`${QQ_API_BASE}/gateway`)\n    const data = (await response.json()) as { url: string }\n    return data.url\n  }\n\n  private async startGateway(): Promise<void> {\n    if (this.isConnecting || this.shouldStop) return\n    this.isConnecting = true\n\n    try {\n      this.cleanup()\n","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/qq/QqAdapter.ts#L187-L223","documentation":"Generic gateway thrown by QqAdapter.apiRequest() after net.fetch to a QQ API endpoint returns non-2xx. Embeds endpoint, status, and the raw errorText (with a .catch fallback to ''). Used by gateway lookup, message send (sendToChat), and other QQ REST calls.","triggerScenarios":"apiRequest() (line ~188) fetches an endpoint with Authorization: QQBot <token> and X-Union-Appid; response.ok is false; errorText is read and the message is thrown. Hit during getGatewayUrl, message POSTs, etc.","commonSituations":"Access token expired (QQ returns 401 — token cache TTL missed), insufficient bot permissions for the target user/group/channel, rate limited, or hitting an endpoint with the wrong X-Union-Appid.","solutions":["On 401, invalidate this.tokenCache and call fetchAccessToken() once, then retry the original request (the cache has expiresAt but a server-side revocation can preempt it).","Read the embedded errorText for QQ's {code, message,...} to find the specific cause (e.g. permission scope missing).","For 429, honor Retry-After / back off.","Verify X-Union-Appid equals this.appId and the endpoint path matches the chat type."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Pre-send: ensure the token cache is fresh.\nif (!qq.tokenFresh()) await qq.refreshToken()\nawait qq.sendToChat(chatId, text)","typeGuard":"function isQqApiHttpError(e: unknown): e is Error {\n  return e instanceof Error && /^QQ API request failed .*: HTTP \\d+/.test(e.message)\n}","tryCatchPattern":"try {\n  await qq.sendToChat(chatId, text)\n} catch (e) {\n  if (isQqApiHttpError(e)) {\n    const status = Number(/HTTP (\\d+)/.exec(e.message)?.[1] ?? 0)\n    if (status === 401) { await qq.invalidateTokenAndRefresh(); return qq.sendToChat(chatId, text) }\n    if (status === 429 || status >= 500) return backoffAndRetry()\n  }\n  throw e\n}","preventionTips":["On 401, invalidate the token cache and refresh once, then retry.","Honor Retry-After/back off on 429 and 5xx.","Verify X-Union-Appid and endpoint path match the chat type."],"tags":["qq","http","auth","rate-limit","api"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}