jackwener/OpenCLI · error · CliError
HTTP_ERROR
HTTP_ERROR
Error message
holders failed: HTTP ${resp.status} What it means
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.
Source
Thrown at clis/eastmoney/holders.js:60
/** @type {string} */
let secucode;
try { secucode = toSecucode(args.symbol); }
catch (err) { throw new CliError('INVALID_ARGUMENT', `${err instanceof Error ? err.message : err}`); }
const limit = Math.max(1, Math.min(Number(args.limit) || 10, 50));
const url = new URL('https://datacenter-web.eastmoney.com/api/data/v1/get');
url.searchParams.set('sortColumns', 'END_DATE,HOLDER_RANK');
url.searchParams.set('sortTypes', '-1,1');
url.searchParams.set('pageSize', String(Math.max(limit, 10)));
url.searchParams.set('pageNumber', '1');
url.searchParams.set('reportName', 'RPT_F10_EH_FREEHOLDERS');
url.searchParams.set('columns', 'SECUCODE,SECURITY_CODE,END_DATE,HOLDER_RANK,HOLDER_NAME,HOLD_NUM,FREE_HOLDNUM_RATIO,HOLD_NUM_CHANGE');
url.searchParams.set('source', 'HSF10');
url.searchParams.set('client', 'PC');
url.searchParams.set('filter', `(SECUCODE="${secucode}")`);
const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
if (!resp.ok) throw new CliError('HTTP_ERROR', `holders failed: HTTP ${resp.status}`);
const data = await resp.json();
const rows = Array.isArray(data?.result?.data) ? data.result.data : [];
if (rows.length === 0) throw new CliError('NO_DATA', `No shareholder data for ${secucode}`);
// Only the most recent reporting period
const latest = String(rows[0].END_DATE || '').slice(0, 10);
return rows
.filter((it) => String(it.END_DATE || '').slice(0, 10) === latest)
.slice(0, limit)
.map((it) => ({
rank: it.HOLDER_RANK,
reportDate: latest,
name: it.HOLDER_NAME,
holdNum: it.HOLD_NUM,
floatRatio: it.FREE_HOLDNUM_RATIO,
change: it.HOLD_NUM_CHANGE,
}));
},View on GitHub (pinned to 49907e53dc)
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.
Example fix
// before
const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
if (!resp.ok) throw new CliError('HTTP_ERROR', `holders failed: HTTP ${resp.status}`);
// after
const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://emweb.securities.eastmoney.com/' } });
if (!resp.ok) {
if (resp.status === 429 || resp.status >= 500) return retryWithBackoff(() => fetchHolders(secucode, limit));
throw new CliError('HTTP_ERROR', `holders failed: HTTP ${resp.status} (secucode=${secucode})`);
} Defensive patterns
Strategy: retry
Validate before calling
// validate the normalized secucode before spending an HTTP request
if (!/^\d{6}\.(SH|SZ|BJ)$/.test(secucode)) throw new CliError('INVALID_ARGUMENT', `bad secucode: ${secucode}`); Type guard
function isTransientHttpError(err) {
return err instanceof CliError && err.code === 'HTTP_ERROR'
&& /HTTP (429|500|502|503|504)/.test(err.message);
} Try / catch
try {
const holders = await fetchHolders({ symbol: '600519' });
} catch (err) {
if (err instanceof CliError && err.code === 'HTTP_ERROR') {
if (isTransientHttpError(err)) return retryWithBackoff(() => fetchHolders({ symbol: '600519' }), 3);
console.error(`eastmoney datacenter rejected request (${err.message}); check IP/headers`);
return;
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: Eastmoney WAF blocking datacenter IPs (403), too-frequent polling (429), temporary outages (502/503), or eastmoney tightening parameter/filter validation after an API change.
Related errors
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/d95e2adef8ee7db8.
Report an issue: GitHub.