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

  1. Retry after a delay; 429 and 5xx are usually transient.
  2. If status is 403, switch to a different network (residential IP) or reduce request rate.
  3. Open the constructed URL in a browser to confirm the endpoint, params, and filter quoting still work.
  4. Verify secucode formatting (e.g. 600519.SH) is exactly what eastmoney expects inside the filter.
  5. 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

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.