jackwener/OpenCLI · error · CommandExecutionError

coingecko derivatives returned malformed JSON

Error message

coingecko derivatives returned malformed JSON: ${err?.message ?? err}

What it means

The coingecko derivatives command wraps the response body parse of GET https://api.coingecko.com/api/v3/derivatives in a try/catch. When resp.json() rejects (body is not valid JSON), it rethrows a CommandExecutionError with the underlying parse message. The library throws this because the HTTP request itself succeeded (status was ok), but the payload could not be decoded.

Solutions

  1. Retry the command after a short wait — transient CDN/proxy hiccups often resolve on their own.
  2. Check the raw response with curl -s https://api.coingecko.com/api/v3/derivatives | head -c 200 to see what body is actually returned.
  3. Disable or bypass intercepting proxies/VPNs (check HTTP(S)_PROXY env vars) that may inject HTML pages.
  4. Reduce request frequency to avoid Cloudflare bot challenges; consider a CoinGecko Pro API key.
  5. Verify network/DNS allows reaching api.coingecko.com directly.

Example fix

// before — caller assumes success means JSON
const data = await runCli(['coingecko', 'derivatives']);
// after
try {
  const data = await runCli(['coingecko', 'derivatives']);
} catch (err) {
  if (String(err.message).includes('malformed JSON')) {
    await sleep(1000); // transient HTML/Cloudflare page — retry
    return retryDerivatives();
  }
  throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const resp = await fetch('https://api.coingecko.com/api/v3/derivatives');
const text = await resp.text();
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
try { JSON.parse(text); } catch { console.warn('Non-JSON body, likely HTML challenge/proxy page'); }

Type guard

function isDerivativeArray(v) {
  return Array.isArray(v) && v.every((d) => d != null && typeof d === 'object' && 'symbol' in d);
}

Try / catch

try {
  const rows = await runCli(['coingecko', 'derivatives']);
} catch (err) {
  if (String(err.message).includes('malformed JSON')) {
    return retryWithBackoff(() => runCli(['coingecko', 'derivatives']), 3);
  }
  throw err;
}

Prevention

When it happens

Trigger: CoinGecko returns HTTP 200 with a non-JSON body — e.g. an HTML Cloudflare challenge/interstitial page, a proxy/captive-portal interception page, a truncated or empty response body, or a maintenance error page served with a 2xx status.

Common situations: Corporate proxies or VPNs injecting an HTML block page; CoinGecko serving a Cloudflare 'checking your browser' page to the CLI's User-Agent; network middleboxes truncating the response mid-stream; CDN outage returning garbage.

Understand the failure class

Related errors


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

Appendix: source

Thrown at clis/coingecko/derivatives.js:57

        }
        catch (err) {
            throw new CommandExecutionError(`coingecko derivatives request failed: ${err?.message ?? err}`);
        }
        if (resp.status === 429) {
            throw new CommandExecutionError(
                'coingecko derivatives returned HTTP 429 (rate limited)',
                'Free tier allows ~30 calls/min. Wait and retry.',
            );
        }
        if (!resp.ok) {
            throw new CommandExecutionError(`coingecko derivatives returned HTTP ${resp.status}`);
        }
        let data;
        try {
            data = await resp.json();
        }
        catch (err) {
            throw new CommandExecutionError(`coingecko derivatives returned malformed JSON: ${err?.message ?? err}`);
        }
        if (!Array.isArray(data) || !data.length) {
            throw new EmptyResultError('coingecko derivatives', 'CoinGecko returned no derivative tickers.');
        }
        let rows = data;
        if (filter) {
            rows = data.filter((d) => String(d.symbol ?? '').toUpperCase().includes(filter)
                || String(d.index_id ?? '').toUpperCase().includes(filter));
            if (!rows.length) {
                throw new EmptyResultError('coingecko derivatives', `No derivative tickers matched symbol="${filter}".`);
            }
        }
        return rows.slice(0, limit).map((d, i) => ({
            rank: i + 1,
            market: String(d.market ?? ''),
            symbol: String(d.symbol ?? ''),
            indexId: String(d.index_id ?? ''),
            contractType: String(d.contract_type ?? ''),

View on GitHub (pinned to 49907e53dc)