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

  1. Inspect both the HTTP status (ApiError.status) and payload — the status code is the primary diagnostic.
  2. 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).
  3. For status 404: the endpoint path changed; verify against current WeChat iLink protocol.
  4. 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

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


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