{"record":{"id":"e48373c5814dd4ac","repo":"jackwener/OpenCLI","slug":"http-error-e48373","errorCode":"HTTP_ERROR","errorMessage":"etf failed: HTTP ${resp.status}","messagePattern":"etf failed: HTTP (.+?)","errorType":"error_code","errorClass":"CliError","httpStatus":null,"severity":"error","filePath":"clis/eastmoney/etf.js","lineNumber":49,"sourceCode":"    const sortKey = String(args.sort ?? 'turnover').toLowerCase();\n    const sort = SORTS[sortKey];\n    if (!sort) throw new CliError('INVALID_ARGUMENT', `Unknown sort \"${sortKey}\". Valid: ${Object.keys(SORTS).join(', ')}`);\n    const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));\n\n    const url = new URL('https://push2.eastmoney.com/api/qt/clist/get');\n    url.searchParams.set('pn', '1');\n    url.searchParams.set('pz', String(limit));\n    url.searchParams.set('po', sort.order === 'desc' ? '1' : '0');\n    url.searchParams.set('np', '1');\n    url.searchParams.set('fltt', '2');\n    url.searchParams.set('invt', '2');\n    url.searchParams.set('fid', sort.fid);\n    url.searchParams.set('fs', 'b:MK0021'); // 场内ETF\n    url.searchParams.set('fields', 'f12,f14,f2,f3,f4,f5,f6,f8');\n    url.searchParams.set('ut', 'bd1d9ddb04089700cf9c27f6f7426281');\n\n    const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });\n    if (!resp.ok) throw new CliError('HTTP_ERROR', `etf failed: HTTP ${resp.status}`);\n    const data = await resp.json();\n    const diff = Array.isArray(data?.data?.diff) ? data.data.diff : [];\n    if (diff.length === 0) throw new CliError('NO_DATA', 'eastmoney returned no ETF data');\n\n    return diff.slice(0, limit).map((it, i) => ({\n      rank: i + 1,\n      code: it.f12,\n      name: it.f14,\n      price: it.f2,\n      changePercent: it.f3,\n      change: it.f4,\n      turnover: it.f6,\n      volume: it.f5,\n      turnoverRate: it.f8,\n    }));\n  },\n});\n","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/eastmoney/etf.js#L31-L67","documentation":"CliError('HTTP_ERROR') thrown in clis/eastmoney/etf.js when the eastmoney push2 ranking API responds with a non-2xx status (resp.ok is false). The library fetches the ranking endpoint with fid/fs=b:MK0021 (场内ETF)/fields/ut params and requires a successful HTTP response before parsing JSON. It signals the remote service rejected or failed the request, not that data was empty (that is NO_DATA).","triggerScenarios":"Any fetch to the eastmoney ETF ranking endpoint returning e.g. 403 (UA/WAF block), 429 (rate limited), 5xx (eastmoney outage), or 404 (endpoint changed) — checked via `if (!resp.ok) throw new CliError('HTTP_ERROR', ...)` at clis/eastmoney/etf.js:49.","commonSituations":"Running from a datacenter IP blocked by eastmoney's WAF (403), hammering the API in a loop and getting throttled (429), transient eastmoney server errors (502/503), or eastmoney deprecating the endpoint or ut token.","solutions":["Re-run after a short delay; many failures are transient 5xx or rate-limit responses.","Read resp.status in the message: 403/429 means IP or header blocking — change network or back off.","Confirm the endpoint and ut token still work by opening the request URL in a browser.","Add retry with exponential backoff for 429/5xx; fail fast on other 4xx.","Update the library/CLI if eastmoney changed the API surface (persistent 404/400)."],"exampleFix":"// before\nconst resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });\nif (!resp.ok) throw new CliError('HTTP_ERROR', `etf failed: HTTP ${resp.status}`);\n// after\nconst resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://quote.eastmoney.com/' } });\nif (!resp.ok) {\n  if (resp.status === 429 || resp.status >= 500) return retryWithBackoff(() => fetchEtf(limit));\n  throw new CliError('HTTP_ERROR', `etf failed: HTTP ${resp.status}`);\n}","handlingStrategy":"retry","validationCode":"const url = new URL('https://push2.eastmoney.com/api/qt/clist/get');\nurl.searchParams.set('fs', 'b:MK0021');\nurl.searchParams.set('ut', 'bd1d9ddb04089700cf9c27f6f7426281');\nconst probe = await fetch(url, { method: 'HEAD' });\nif (!probe.ok) throw new Error(`eastmoney unreachable: HTTP ${probe.status}`);","typeGuard":"function isOkResponse(resp) { return typeof resp === 'object' && resp !== null && 'ok' in resp && resp.ok === true; }","tryCatchPattern":"try {\n  const etf = await fetchEtf({ limit: 10 });\n} catch (err) {\n  if (err instanceof CliError && err.code === 'HTTP_ERROR') {\n    if (/HTTP (429|5\\d\\d)/.test(err.message)) return retryWithBackoff(() => fetchEtf({ limit: 10 }), 3);\n    console.error(`eastmoney request failed (${err.message}); check network/IP or API status`);\n    return;\n  }\n  throw err;\n}","preventionTips":["Throttle requests to eastmoney (add delays) to avoid 429 rate limits.","Send browser-like headers (User-Agent, Referer) to reduce 403 WAF blocks.","Implement retry with exponential backoff for 429/5xx only.","Monitor eastmoney API health; alert on persistent non-2xx responses.","Catch CliError.code === 'HTTP_ERROR' specifically rather than generic errors."],"tags":["network","http","eastmoney","api"],"backgroundTag":"upstream-http-error","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}