CherryHQ/cherry-studio · error · ApiError
${label} failed with HTTP ${response.status}
Error message
${label} failed with HTTP ${response.status} What it means
Thrown as an ApiError by parseJsonResponse() when the HTTP status is not OK (non-2xx) AND the parsed JSON body either does not match ApiErrorBodySchema or has no errmsg field. The fallback message uses the raw HTTP status. So this fires when WeChat iLink returns a non-2xx status with a body that is JSON but not in the expected {ret, errcode, errmsg} error shape.
Source
Thrown at src/main/ai/channels/adapters/wechat/WeChatProtocol.ts:390
return { channel_version: CHANNEL_VERSION }
}
async function parseJsonResponse(response: Response, label: string): Promise<unknown> {
const text = await response.text()
let raw: unknown
try {
raw = text ? JSON.parse(text) : {}
} catch {
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
}
View on GitHub (pinned to 726446b54c)
Solutions
- Inspect both the HTTP status (ApiError.status) and payload — the status code is the primary diagnostic.
- For status 401/403: the bot token in credentials is stale; trigger a re-login (the runLoop already does this on code -14, but a 401 may carry a different shape).
- For status 404: the endpoint path changed; verify against current WeChat iLink protocol.
- For 5xx: transient, the existing retryDelayMs backoff in runLoop handles it.
Example fix
// before — generic message, status is on the error but not surfaced in the text
throw new ApiError(parsed.errmsg ?? `${label} failed with HTTP ${response.status}`, {
status: response.status, code: parsed.errcode, payload: raw
})
// after — include the body keys so the shape mismatch is obvious
const bodyKeys = raw && typeof raw === 'object' ? Object.keys(raw).slice(0, 5).join(',') : 'n/a'
throw new ApiError(
parsed.errmsg ?? `${label} failed with HTTP ${response.status} (body keys: ${bodyKeys})`,
{ status: response.status, code: parsed.errcode, payload: raw }
) Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-validate the credentials (token, uin, baseUrl) before the call to reduce 4xx.
// The headers are built by buildHeaders (WeChatProtocol.ts:409); verify inputs:
function hasValidWeChatHeaders(token: string, uin: string): boolean {
return typeof token === 'string' && token.length > 0 &&
typeof uin === 'string' && uin.length > 0
}
if (!hasValidWeChatHeaders(credentials.token, bot.uin)) {
throw new Error('WeChat credentials incomplete — re-login required')
} Type guard
function isHttpFailureWithStatus(e: unknown, status: number): boolean {
return e instanceof ApiError && e.status === status && /failed with HTTP/.test(e.message)
}
function isAuthFailure(e: unknown): boolean {
return isHttpFailureWithStatus(e, 401) || isHttpFailureWithStatus(e, 403)
} Try / catch
// In runLoop, auth failures should trigger a re-login (similar to code -14)
try {
await getUpdates(baseUrl, token, uin, cursor, signal)
} catch (e) {
if (isAuthFailure(e)) {
logger.info('WeChat auth failed (HTTP 401/403), forcing re-login')
await bot.login({ force: true })
continue
}
throw e
} Prevention
- Inspect ApiError.status first — it carries the primary diagnostic for this variant.
- On 401/403, trigger a forced re-login rather than retrying the same credentials.
- On 404, verify the endpoint path against the current WeChat iLink protocol (reverse-engineered, may shift).
- For 5xx, lean on runLoop's exponential backoff rather than adding parallel retries.
When it happens
Trigger: POST/GET to a /ilink/bot/* endpoint returns 4xx/5xx with a JSON body that lacks errmsg. Examples: 401 with {'foo':'bar'}, 500 with {'error':'something'} (wrong field name), 404 with an empty object {} (caught by the `text ? JSON.parse : {}` path, so empty body yields {} which has no errmsg → this fallback). The status field is set on the ApiError so callers can inspect it.
Common situations: The token expired and WeChat returns 401 with a non-standard body; the endpoint URL changed (404 with HTML body would be error 132, but a 404 JSON body hits here); rate limiting with a non-standard JSON shape; the uin header (X-WECHAT-UIN) is malformed and the server rejects with 400.
Related errors
- ${label} returned non-JSON (HTTP ${response.status})
- ${label} failed
- Slack API error ${method}: HTTP ${response.status} - ${error
- QR login confirmed, but the API did not return bot credentia
- Failed to upload image to WeChat CDN
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/c0e3f691bb640f30.
Report an issue: GitHub.