jackwener/OpenCLI · error · CommandExecutionError

${label} returned HTTP ${resp.status}

Error message

${label} returned HTTP ${resp.status}

What it means

After the 429 check, juejinFetch requires `resp.ok` (2xx). Any other HTTP status (403, 404, 5xx, etc.) is thrown as a CommandExecutionError carrying the status code. Unlike 429 it has no remediation hint, because the meaning depends entirely on the endpoint and status.

Source

Thrown at clis/juejin/utils.js:111

        if (method === 'POST') {
            init.headers['content-type'] = 'application/json';
            init.body = JSON.stringify(body ?? {});
        }
        resp = await fetch(url, init);
    } catch (err) {
        throw new CommandExecutionError(
            `${label} request failed: ${err?.message ?? err}`,
            'Check that api.juejin.cn is reachable from this network.',
        );
    }
    if (resp.status === 429) {
        throw new CommandExecutionError(
            `${label} returned HTTP 429 (rate limited)`,
            'Juejin throttles bursty traffic; wait a few seconds and retry.',
        );
    }
    if (!resp.ok) {
        throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
    }
    let payload;
    try {
        payload = await resp.json();
    } catch (err) {
        throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
    }
    if (!payload || typeof payload !== 'object' || Array.isArray(payload) || !Object.hasOwn(payload, 'err_no')) {
        throw new CommandExecutionError(`${label} returned a malformed API envelope`);
    }
    if (payload.err_no !== 0) {
        throw new CommandExecutionError(`${label} returned err_no ${payload.err_no}: ${payload.err_msg ?? ''}`);
    }
    return payload;
}

export function readDataArray(payload, label) {
    if (!payload || typeof payload !== 'object' || Array.isArray(payload) || !Object.hasOwn(payload, 'data')) {

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Check the status in the message: 403/405 usually means WAF blocking — try from a residential IP or adjust the user-agent.
  2. For 5xx, treat it as a transient Juejin outage: retry after a minute; check Juejin's status.
  3. For 404, verify the endpoint path in the calling adapter code hasn't been retired by a Juejin API change; update the path.
  4. Confirm JUEJIN_API_BASE is unchanged and correct (https://api.juejin.cn).
  5. Reproduce with `curl -X POST -H 'content-type: application/json' -d '{}' <url>` to see the raw status/body and any WAF challenge page.

Example fix

// before
throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);

// after (caller handles transient 5xx with backoff)
try {
  const payload = await juejinFetch('/recommend_api/feed/v1', body, 'juejin recommend');
} catch (e) {
  if (/HTTP 5\d\d/.test(e.message)) await sleep(5000), retry();
  else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// optional pre-flight endpoint sanity check
async function endpointOk(path) {
  try {
    const r = await fetch(`https://api.juejin.cn${path}`, {
      method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}',
      signal: AbortSignal.timeout(5000),
    });
    return r.status !== 404 && r.status !== 405; // path still exists
  } catch { return false; }
}

Type guard

function isHttpStatusError(err) {
  const m = err instanceof CommandExecutionError && err.message.match(/returned HTTP (\d{3})/);
  return m ? { isMatch: true, status: Number(m[1]) } : { isMatch: false, status: null };
}

Try / catch

try {
  const payload = await juejinFetch(path, body, label);
} catch (err) {
  const { isMatch, status } = isHttpStatusError(err);
  if (isMatch && status >= 500) {
    console.error('Juejin server error — retry later.');
  } else if (isMatch && (status === 403 || status === 405)) {
    console.error('Blocked or unsupported request — check egress IP / adapter version.');
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: api.juejin.cn returned a non-ok, non-429 status to a juejinFetch call: e.g. 403 from a WAF/anti-bot layer, 404 from a changed/retired endpoint path, 5xx from a Juejin server-side outage, or 301/30x mishandling if the API surface moved.

Common situations: Juejin's WAF returning 403 to datacenter/VPN IPs; the adapter's endpoint path breaking after a Juejin API redesign; transient 502/503 during Juejin deploy windows; wrong JUEJIN_API_BASE if someone points it at a mirror.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/039650fe1479d8c2. Report an issue: GitHub.