{"record":{"id":"497ab751986fd407","repo":"CherryHQ/cherry-studio","slug":"invalid-token-response-from-qq-api-errortext","errorCode":null,"errorMessage":"Invalid token response from QQ API: ${errorText}","messagePattern":"Invalid token response from QQ API: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/qq/QqAdapter.ts","lineNumber":177,"sourceCode":"    }\n\n    const response = await net.fetch('https://bots.qq.com/app/getAppAccessToken', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        appId: this.appId,\n        clientSecret: this.clientSecret\n      })\n    })\n\n    if (!response.ok) {\n      throw new Error(`Failed to get access token: HTTP ${response.status}`)\n    }\n\n    const data = (await response.json()) as { access_token?: string; expires_in?: number }\n    if (!data.access_token || !data.expires_in) {\n      const errorText = JSON.stringify(data)\n      throw new Error(`Invalid token response from QQ API: ${errorText}`)\n    }\n\n    this.tokenCache = {\n      accessToken: data.access_token,\n      expiresAt: Date.now() + data.expires_in * 1000\n    }\n\n    return data.access_token\n  }\n\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: {","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/qq/QqAdapter.ts#L159-L195","documentation":"Thrown when getAppAccessToken returns 2xx but the JSON body lacks access_token or expires_in. The whole response is JSON.stringify-d into the message as errorText. This catches a structurally-invalid success response rather than an HTTP failure.","triggerScenarios":"response.ok is true, data = await response.json(), but data.access_token or data.expires_in is falsy; the guard throws the stringified body.","commonSituations":"QQ API changed its token response schema, returned an error object inside a 200 body (e.g. {code, message} without the token fields), or a proxy returned 200 with an HTML/empty JSON payload.","solutions":["Read the embedded errorText to see QQ's actual 200-body shape (often an inline {code,message} error).","If QQ returned an inline error, address the root cause (e.g. invalid clientSecret) — the HTTP status alone was misleading.","If the schema changed, update the type cast and field reads to match the new response.","Retry once for transient QQ inconsistencies."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"// Validate the parsed token shape before trusting it.\nfunction isQqTokenShape(d: unknown): d is { access_token: string; expires_in: number } {\n  return typeof d === 'object' && d !== null\n    && typeof (d as any).access_token === 'string'\n    && typeof (d as any).expires_in === 'number'\n}","typeGuard":"function isQqInvalidTokenResponse(e: unknown): e is Error {\n  return e instanceof Error && e.message.startsWith('Invalid token response from QQ API:')\n}","tryCatchPattern":"try {\n  await qq.connect()\n} catch (e) {\n  if (isQqInvalidTokenResponse(e)) { notifyUser('QQ returned an unexpected token response; retry later'); return }\n  throw e\n}","preventionTips":["Treat a 200 body lacking token fields as an inline QQ error, not a transient blip.","Keep the token-shape type guard in sync with QQ's documented response.","Log the embedded errorText so schema drift is visible."],"tags":["qq","auth","schema","validation","token"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}