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

  1. Inspect ApiError.code — if -14, the runLoop auto-re-login path (WeChatProtocol.ts:900) handles it; ensure the error propagates there.
  2. For other codes, log code + payload and surface to the user; the codes are undocumented (reverse-engineered).
  3. If the context_token is stale, clearing contextTokens and waiting for a fresh inbound message before sending again resolves it.
  4. 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

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


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/2a56986a7d00a309. Report an issue: GitHub.