{"record":{"id":"d95e2adef8ee7db8","repo":"jackwener/OpenCLI","slug":"http-error-d95e2a","errorCode":"HTTP_ERROR","errorMessage":"holders failed: HTTP ${resp.status}","messagePattern":"holders failed: HTTP (.+?)","errorType":"error_code","errorClass":"CliError","httpStatus":null,"severity":"error","filePath":"clis/eastmoney/holders.js","lineNumber":60,"sourceCode":"    /** @type {string} */\n    let secucode;\n    try { secucode = toSecucode(args.symbol); }\n    catch (err) { throw new CliError('INVALID_ARGUMENT', `${err instanceof Error ? err.message : err}`); }\n    const limit = Math.max(1, Math.min(Number(args.limit) || 10, 50));\n\n    const url = new URL('https://datacenter-web.eastmoney.com/api/data/v1/get');\n    url.searchParams.set('sortColumns', 'END_DATE,HOLDER_RANK');\n    url.searchParams.set('sortTypes', '-1,1');\n    url.searchParams.set('pageSize', String(Math.max(limit, 10)));\n    url.searchParams.set('pageNumber', '1');\n    url.searchParams.set('reportName', 'RPT_F10_EH_FREEHOLDERS');\n    url.searchParams.set('columns', 'SECUCODE,SECURITY_CODE,END_DATE,HOLDER_RANK,HOLDER_NAME,HOLD_NUM,FREE_HOLDNUM_RATIO,HOLD_NUM_CHANGE');\n    url.searchParams.set('source', 'HSF10');\n    url.searchParams.set('client', 'PC');\n    url.searchParams.set('filter', `(SECUCODE=\"${secucode}\")`);\n\n    const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });\n    if (!resp.ok) throw new CliError('HTTP_ERROR', `holders failed: HTTP ${resp.status}`);\n    const data = await resp.json();\n    const rows = Array.isArray(data?.result?.data) ? data.result.data : [];\n    if (rows.length === 0) throw new CliError('NO_DATA', `No shareholder data for ${secucode}`);\n\n    // Only the most recent reporting period\n    const latest = String(rows[0].END_DATE || '').slice(0, 10);\n    return rows\n      .filter((it) => String(it.END_DATE || '').slice(0, 10) === latest)\n      .slice(0, limit)\n      .map((it) => ({\n        rank: it.HOLDER_RANK,\n        reportDate: latest,\n        name: it.HOLDER_NAME,\n        holdNum: it.HOLD_NUM,\n        floatRatio: it.FREE_HOLDNUM_RATIO,\n        change: it.HOLD_NUM_CHANGE,\n      }));\n  },","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/eastmoney/holders.js#L42-L78","documentation":"CliError('HTTP_ERROR') thrown in clis/eastmoney/holders.js when the datacenter-web.eastmoney.com API (source=HSF10, client=PC, filter `(SECUCODE=\"<code>\")`) returns a non-2xx status. Since the filter is built from an already-normalized secucode, an HTTP failure means the request was rejected upstream rather than the symbol being invalid. The status code in the message distinguishes blocking/throttling from server faults.","triggerScenarios":"fetch to https://datacenter-web.eastmoney.com/api/data/v1/get with sortColumns/sortTypes/pageSize/columns/source/client/filter params returning 403/429/5xx (or 400 on a malformed filter) — checked at `if (!resp.ok) throw new CliError('HTTP_ERROR', ...)` holders.js:60.","commonSituations":"Eastmoney WAF blocking datacenter IPs (403), too-frequent polling (429), temporary outages (502/503), or eastmoney tightening parameter/filter validation after an API change.","solutions":["Retry after a delay; 429 and 5xx are usually transient.","If status is 403, switch to a different network (residential IP) or reduce request rate.","Open the constructed URL in a browser to confirm the endpoint, params, and filter quoting still work.","Verify secucode formatting (e.g. 600519.SH) is exactly what eastmoney expects inside the filter.","Add retry with exponential backoff for 429/5xx, no retry for other 4xx, and include status+filter in the error."],"exampleFix":"// before\nconst resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });\nif (!resp.ok) throw new CliError('HTTP_ERROR', `holders failed: HTTP ${resp.status}`);\n// after\nconst resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://emweb.securities.eastmoney.com/' } });\nif (!resp.ok) {\n  if (resp.status === 429 || resp.status >= 500) return retryWithBackoff(() => fetchHolders(secucode, limit));\n  throw new CliError('HTTP_ERROR', `holders failed: HTTP ${resp.status} (secucode=${secucode})`);\n}","handlingStrategy":"retry","validationCode":"// validate the normalized secucode before spending an HTTP request\nif (!/^\\d{6}\\.(SH|SZ|BJ)$/.test(secucode)) throw new CliError('INVALID_ARGUMENT', `bad secucode: ${secucode}`);","typeGuard":"function isTransientHttpError(err) {\n  return err instanceof CliError && err.code === 'HTTP_ERROR'\n    && /HTTP (429|500|502|503|504)/.test(err.message);\n}","tryCatchPattern":"try {\n  const holders = await fetchHolders({ symbol: '600519' });\n} catch (err) {\n  if (err instanceof CliError && err.code === 'HTTP_ERROR') {\n    if (isTransientHttpError(err)) return retryWithBackoff(() => fetchHolders({ symbol: '600519' }), 3);\n    console.error(`eastmoney datacenter rejected request (${err.message}); check IP/headers`);\n    return;\n  }\n  throw err;\n}","preventionTips":["Retry with exponential backoff only for 429/5xx; never for 4xx.","Include a Referer header matching the eastmoney web client to avoid 403s.","Rate-limit polling of the datacenter API (e.g. >=1s between calls).","Verify the SECUCODE filter format (600519.SH) stays valid after eastmoney changes.","Catch CliError.code === 'HTTP_ERROR' and branch on the status code embedded in the message."],"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"}