CherryHQ/cherry-studio · error · ApiError
${label} failed
Error message
${label} failed What it means
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.
Source
Thrown at src/main/ai/channels/adapters/wechat/WeChatProtocol.ts:399
throw new ApiError(`${label} returned non-JSON (HTTP ${response.status})`, {
status: response.status,
payload: text.slice(0, 200)
})
}
if (!response.ok) {
const body = ApiErrorBodySchema.safeParse(raw)
const parsed = body.success ? body.data : {}
throw new ApiError(parsed.errmsg ?? `${label} failed with HTTP ${response.status}`, {
status: response.status,
code: parsed.errcode,
payload: raw
})
}
const body = ApiErrorBodySchema.safeParse(raw)
if (body.success && typeof body.data.ret === 'number' && body.data.ret !== 0) {
throw new ApiError(body.data.errmsg ?? `${label} failed`, {
status: response.status,
code: body.data.errcode ?? body.data.ret,
payload: raw
})
}
return raw
}
function buildHeaders(token: string, uin: string): Record<string, string> {
return {
'Content-Type': 'application/json',
AuthorizationType: 'ilink_bot_token',
Authorization: `Bearer ${token}`,
'X-WECHAT-UIN': uin
}
}
View on GitHub (pinned to 726446b54c)
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.
Example fix
// before — generic message hides the numeric code in the thrown text
throw new ApiError(body.data.errmsg ?? `${label} failed`, {
status: response.status, code: body.data.errcode ?? body.data.ret, payload: raw
})
// after — always include ret/code in the message for undocumented codes
const code = body.data.errcode ?? body.data.ret
throw new ApiError(body.data.errmsg ?? `${label} failed (ret=${code})`, {
status: response.status, code, payload: raw
}) Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-validate that the context_token exists and the user id is non-empty before sending.
// WeChat sendText requires a context_token (WeChatProtocol.ts:766 warns if missing).
function hasWeChatSendContext(contextToken: string | undefined, userId: string): boolean {
return typeof contextToken === 'string' && contextToken.length > 0 &&
typeof userId === 'string' && userId.length > 0
}
const ctx = bot.contextTokens.get(userId)
if (!hasWeChatSendContext(ctx, userId)) {
// Wait for an inbound message to cache the token rather than sending without context
logger.warn('No context token for user, skipping send', { userId })
return
} Type guard
// Session-expired (code -14) is the well-known recoverable case
function isSessionExpiredError(e: unknown): boolean {
return e instanceof ApiError && e.code === -14
}
function isNonZeroRetError(e: unknown): boolean {
return e instanceof ApiError && typeof e.code === 'number' && e.code !== 0 && /failed/.test(e.message)
} Try / catch
// runLoop already handles -14 by clearing creds and re-logging in (WeChatProtocol.ts:900).
// For other codes, log the code and surface; do not retry blindly.
try {
await bot.send(userId, text)
} catch (e) {
if (isSessionExpiredError(e)) {
await bot.login({ force: true }) // runLoop's path
await bot.send(userId, text) // retry once
return
}
if (isNonZeroRetError(e)) {
logger.error('WeChat send failed with undocumented code', { code: (e as ApiError).code, payload: (e as ApiError).payload })
}
throw e
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ${label} returned non-JSON (HTTP ${response.status})
- ${label} failed with HTTP ${response.status}
- QR login confirmed, but the API did not return bot credentia
- Socket Mode connection failed: ${data.error ?? 'no URL retur
- Slack API error ${method}: HTTP ${response.status} - ${error
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/2a56986a7d00a309.
Report an issue: GitHub.