{"record":{"id":"c0e3f691bb640f30","repo":"CherryHQ/cherry-studio","slug":"label-failed-with-http-response-status","errorCode":null,"errorMessage":"${label} failed with HTTP ${response.status}","messagePattern":"(.+?) failed with HTTP (.+?)","errorType":"exception","errorClass":"ApiError","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/wechat/WeChatProtocol.ts","lineNumber":390,"sourceCode":"  return { channel_version: CHANNEL_VERSION }\n}\n\nasync function parseJsonResponse(response: Response, label: string): Promise<unknown> {\n  const text = await response.text()\n  let raw: unknown\n  try {\n    raw = text ? JSON.parse(text) : {}\n  } catch {\n    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","sourceCodeStart":372,"sourceCodeEnd":408,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/wechat/WeChatProtocol.ts#L372-L408","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — generic message, status is on the error but not surfaced in the text\nthrow new ApiError(parsed.errmsg ?? `${label} failed with HTTP ${response.status}`, {\n  status: response.status, code: parsed.errcode, payload: raw\n})\n\n// after — include the body keys so the shape mismatch is obvious\nconst bodyKeys = raw && typeof raw === 'object' ? Object.keys(raw).slice(0, 5).join(',') : 'n/a'\nthrow new ApiError(\n  parsed.errmsg ?? `${label} failed with HTTP ${response.status} (body keys: ${bodyKeys})`,\n  { status: response.status, code: parsed.errcode, payload: raw }\n)","handlingStrategy":"try-catch","validationCode":"// Pre-validate the credentials (token, uin, baseUrl) before the call to reduce 4xx.\n// The headers are built by buildHeaders (WeChatProtocol.ts:409); verify inputs:\nfunction hasValidWeChatHeaders(token: string, uin: string): boolean {\n  return typeof token === 'string' && token.length > 0 &&\n         typeof uin === 'string' && uin.length > 0\n}\n\nif (!hasValidWeChatHeaders(credentials.token, bot.uin)) {\n  throw new Error('WeChat credentials incomplete — re-login required')\n}","typeGuard":"function isHttpFailureWithStatus(e: unknown, status: number): boolean {\n  return e instanceof ApiError && e.status === status && /failed with HTTP/.test(e.message)\n}\n\nfunction isAuthFailure(e: unknown): boolean {\n  return isHttpFailureWithStatus(e, 401) || isHttpFailureWithStatus(e, 403)\n}","tryCatchPattern":"// In runLoop, auth failures should trigger a re-login (similar to code -14)\ntry {\n  await getUpdates(baseUrl, token, uin, cursor, signal)\n} catch (e) {\n  if (isAuthFailure(e)) {\n    logger.info('WeChat auth failed (HTTP 401/403), forcing re-login')\n    await bot.login({ force: true })\n    continue\n  }\n  throw e\n}","preventionTips":["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."],"tags":["wechat","network","api-error","http-status","protocol"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}