{"record":{"id":"989f45c0f97d744d","repo":"CherryHQ/cherry-studio","slug":"failed-to-upload-image-to-wechat-cdn","errorCode":null,"errorMessage":"Failed to upload image to WeChat CDN","messagePattern":"Failed to upload image to WeChat CDN","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/wechat/WeChatProtocol.ts","lineNumber":819,"sourceCode":"    } catch (error) {\n      logger.error('Failed to download WeChat file', error instanceof Error ? error : { error: String(error) })\n      return null\n    }\n  }\n\n  /**\n   * Send an image to a user by uploading to WeChat CDN.\n   */\n  async sendImage(userId: string, imageData: Buffer): Promise<void> {\n    const contextToken = this.contextTokens.get(userId)\n    if (!contextToken) {\n      logger.warn('No cached context token for sendImage, sending without context', { userId })\n    }\n\n    const credentials = await this.ensureCredentials()\n    const uploaded = await cdnUploadImage(this.baseUrl, credentials.token, this.uin, userId, imageData)\n    if (!uploaded) {\n      throw new Error('Failed to upload image to WeChat CDN')\n    }\n\n    const msg = {\n      from_user_id: '',\n      to_user_id: userId,\n      client_id: randomUUID(),\n      message_type: MessageType.BOT,\n      message_state: MessageState.FINISH,\n      context_token: contextToken ?? '',\n      item_list: [\n        {\n          type: MessageItemType.IMAGE,\n          image_item: {\n            media: {\n              encrypt_query_param: uploaded.downloadEncryptedQueryParam,\n              aes_key: Buffer.from(uploaded.aeskey).toString('base64'),\n              encrypt_type: 1\n            },","sourceCodeStart":801,"sourceCodeEnd":837,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/wechat/WeChatProtocol.ts#L801-L837","documentation":"Thrown by WeixinBot.sendImage() after cdnUploadImage() returns null. The upload pipeline first requests an upload URL from the WeChat /ilink/bot/getuploadurl endpoint, then AES-ECB-encrypts the image and POSTs it to the WeChat CDN. A null return means either the CDN responded with a non-2xx HTTP status, or the response was missing the required x-encrypted-param download header. The error message intentionally hides the underlying HTTP status to avoid leaking CDN internals.","triggerScenarios":"Called from WeChatAdapter.sendFile() which decodes a base64 FileAttachment and passes it to bot.sendImage(chatId, buffer). Fails when: (1) the CDN upload POST at CDN_BASE_URL/upload returns HTTP >= 400 (line 341-343), or (2) the 200 response lacks the x-encrypted-param response header (line 346-350), or (3) the upstream getuploadurl call itself fails (which would throw before reaching the null check). Can also fire transiently under proxy misconfiguration or network disruption.","commonSituations":"WeChat CDN is temporarily unreachable or rate-limiting the bot account; the session token expired mid-upload (cdnUploadImage uses credentials.token which may have rotated since ensureCredentials); a corporate proxy or firewall strips the x-encrypted-param response header; the image data is corrupt or zero-length causing the CDN to reject it; the WeChat bot account has insufficient media-upload quota.","solutions":["Check whether the WeChat CDN endpoint is reachable from the host (network/proxy/VPN) and retry — transient CDN 5xx or timeouts are the most common cause.","Verify credentials are still valid by calling ensureCredentials() before sendImage; an expired token can cause the getuploadurl step to return an upload_param that the CDN rejects.","Inspect the logger.error output at the line just before the throw — cdnUploadImage logs either 'CDN upload failed' with the HTTP status, or 'CDN upload response missing x-encrypted-param header', which pinpoints the exact failure branch.","If the image Buffer is empty or truncated upstream, validate imageData.length > 0 before calling sendImage.","If running behind a proxy, ensure the proxy does not strip or rewrite the x-encrypted-param response header from the CDN."],"exampleFix":"// before\nawait this.bot.sendImage(chatId, Buffer.from(file.data, 'base64'))\n\n// after — guard empty buffer and surface the CDN failure branch\nconst buf = Buffer.from(file.data, 'base64')\nif (buf.length === 0) throw new Error('Cannot send an empty image to WeChat')\nawait this.bot.sendImage(chatId, buf)","handlingStrategy":"try-catch","validationCode":"// Validate image data before calling sendImage\nif (!imageData || imageData.length === 0) {\n  throw new Error('Image data is empty — cannot upload to WeChat CDN')\n}\n// Optionally check a reasonable size ceiling\nif (imageData.length > 20 * 1024 * 1024) {\n  logger.warn('Large image may be rejected by WeChat CDN', { size: imageData.length })\n}","typeGuard":null,"tryCatchPattern":"try {\n  await this.bot.sendImage(chatId, imageData)\n} catch (error) {\n  if (error instanceof Error && error.message === 'Failed to upload image to WeChat CDN') {\n    logger.error('WeChat CDN upload failed, falling back to text notification', { chatId })\n    await this.bot.reply({ userId: chatId, _contextToken: '' }, '[Image delivery failed]')\n  } else {\n    throw error\n  }\n}","preventionTips":["Ensure credentials are fresh by calling ensureCredentials() immediately before sendImage.","Verify network connectivity to the WeChat CDN endpoint before attempting uploads.","Check that the proxy configuration does not strip the x-encrypted-param response header.","Log the HTTP status from cdnUploadImage failures (it already logs via logger.error) to distinguish network errors from auth errors."],"tags":["wechat","network","cdn","image-upload","channels"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}