{"record":{"id":"2a56986a7d00a309","repo":"CherryHQ/cherry-studio","slug":"label-failed","errorCode":null,"errorMessage":"${label} failed","messagePattern":"(.+?) failed","errorType":"exception","errorClass":"ApiError","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/wechat/WeChatProtocol.ts","lineNumber":399,"sourceCode":"    throw new ApiError(`${label} returned non-JSON (HTTP ${response.status})`, {\n      status: response.status,\n      payload: text.slice(0, 200)\n    })\n  }\n\n  if (!response.ok) {\n    const body = ApiErrorBodySchema.safeParse(raw)\n    const parsed = body.success ? body.data : {}\n    throw new ApiError(parsed.errmsg ?? `${label} failed with HTTP ${response.status}`, {\n      status: response.status,\n      code: parsed.errcode,\n      payload: raw\n    })\n  }\n\n  const body = ApiErrorBodySchema.safeParse(raw)\n  if (body.success && typeof body.data.ret === 'number' && body.data.ret !== 0) {\n    throw new ApiError(body.data.errmsg ?? `${label} failed`, {\n      status: response.status,\n      code: body.data.errcode ?? body.data.ret,\n      payload: raw\n    })\n  }\n\n  return raw\n}\n\nfunction buildHeaders(token: string, uin: string): Record<string, string> {\n  return {\n    'Content-Type': 'application/json',\n    AuthorizationType: 'ilink_bot_token',\n    Authorization: `Bearer ${token}`,\n    'X-WECHAT-UIN': uin\n  }\n}\n","sourceCodeStart":381,"sourceCodeEnd":417,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/wechat/WeChatProtocol.ts#L381-L417","documentation":"Thrown as an ApiError by parseJsonResponse() when HTTP status is 2xx, body parsed as JSON, ApiErrorBodySchema matches, but the ret field is a non-zero number AND errmsg is absent. This is WeChat iLink's application-level error where the server returned 200 OK with {ret: <non-zero>, ...} but no human-readable errmsg. The ret and errcode values are the diagnostic.","triggerScenarios":"A successful HTTP 200 response where the body has ret !== 0 (WeChat iLink signals errors via ret=0 for success, non-zero for failure) but the server did not include errmsg. Common ret codes: -14 means session expired (isSessionExpired at WeChatProtocol.ts:1093 checks ApiError.code === -14 and triggers re-login). Other non-zero ret values indicate various protocol/logic errors. The code field is set to errcode ?? ret.","commonSituations":"Session token expired (ret/errcode -14) — handled by runLoop re-login but surfaces here if not in the polling loop; the to_user_id is invalid; the context_token is stale or mismatched; rate/quota limits return a non-zero ret; an undocumented ret code from a protocol change.","solutions":["Inspect ApiError.code — if -14, the runLoop auto-re-login path (WeChatProtocol.ts:900) handles it; ensure the error propagates there.","For other codes, log code + payload and surface to the user; the codes are undocumented (reverse-engineered).","If the context_token is stale, clearing contextTokens and waiting for a fresh inbound message before sending again resolves it.","Treat non-zero ret as authoritative failure — do not retry blindly without code-specific handling."],"exampleFix":"// before — generic message hides the numeric code in the thrown text\nthrow new ApiError(body.data.errmsg ?? `${label} failed`, {\n  status: response.status, code: body.data.errcode ?? body.data.ret, payload: raw\n})\n\n// after — always include ret/code in the message for undocumented codes\nconst code = body.data.errcode ?? body.data.ret\nthrow new ApiError(body.data.errmsg ?? `${label} failed (ret=${code})`, {\n  status: response.status, code, payload: raw\n})","handlingStrategy":"try-catch","validationCode":"// Pre-validate that the context_token exists and the user id is non-empty before sending.\n// WeChat sendText requires a context_token (WeChatProtocol.ts:766 warns if missing).\nfunction hasWeChatSendContext(contextToken: string | undefined, userId: string): boolean {\n  return typeof contextToken === 'string' && contextToken.length > 0 &&\n         typeof userId === 'string' && userId.length > 0\n}\n\nconst ctx = bot.contextTokens.get(userId)\nif (!hasWeChatSendContext(ctx, userId)) {\n  // Wait for an inbound message to cache the token rather than sending without context\n  logger.warn('No context token for user, skipping send', { userId })\n  return\n}","typeGuard":"// Session-expired (code -14) is the well-known recoverable case\nfunction isSessionExpiredError(e: unknown): boolean {\n  return e instanceof ApiError && e.code === -14\n}\n\nfunction isNonZeroRetError(e: unknown): boolean {\n  return e instanceof ApiError && typeof e.code === 'number' && e.code !== 0 && /failed/.test(e.message)\n}","tryCatchPattern":"// runLoop already handles -14 by clearing creds and re-logging in (WeChatProtocol.ts:900).\n// For other codes, log the code and surface; do not retry blindly.\ntry {\n  await bot.send(userId, text)\n} catch (e) {\n  if (isSessionExpiredError(e)) {\n    await bot.login({ force: true }) // runLoop's path\n    await bot.send(userId, text) // retry once\n    return\n  }\n  if (isNonZeroRetError(e)) {\n    logger.error('WeChat send failed with undocumented code', { code: (e as ApiError).code, payload: (e as ApiError).payload })\n  }\n  throw e\n}","preventionTips":["Inspect ApiError.code — -14 means session expired (auto-recover via re-login); other codes are undocumented, log them.","Ensure a context_token is cached for the target user (populated from inbound messages) before sending.","Clear stale contextTokens on re-login (WeixinBot.login does this at WeChatProtocol.ts:723) to avoid ret-related failures.","Do not retry sends on unknown non-zero ret codes without understanding them — they may be quota/permission failures."],"tags":["wechat","api-error","protocol","session","error-code"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}