jackwener/OpenCLI · error · CommandExecutionError

${label} returned malformed JSON: ${err?.message ?? err}

Error message

${label} returned malformed JSON: ${err?.message ?? err}

What it means

readJson calls resp.json() on the OSV API response; if the body is not valid JSON (HTML error pages, truncated responses, proxies), it wraps the parse failure in a CommandExecutionError prefixed with the request label.

Source

Thrown at clis/osv/utils.js:92

export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
    const raw = value ?? defaultValue;
    const n = typeof raw === 'number' ? raw : Number(raw);
    if (!Number.isInteger(n) || n <= 0) {
        throw new ArgumentError(`osv ${label} must be a positive integer`);
    }
    if (n > maxValue) {
        throw new ArgumentError(`osv ${label} must be <= ${maxValue}`);
    }
    return n;
}

async function readJson(resp, label) {
    let body;
    try {
        body = await resp.json();
    }
    catch (err) {
        throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
    }
    return body;
}

export async function osvGet(url, label) {
    let resp;
    try {
        resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
    }
    catch (err) {
        throw new CommandExecutionError(
            `${label} request failed: ${err?.message ?? err}`,
            'Check that api.osv.dev is reachable from this network.',
        );
    }
    if (resp.status === 404) {
        throw new EmptyResultError(label, `OSV.dev returned 404 for ${url}.`);
    }

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Retry the request; transient truncation is common.
  2. Check whether a proxy/firewall is intercepting api.osv.dev (curl the URL and inspect the body).
  3. Bypass or correctly configure proxy env vars (HTTP_PROXY/HTTPS_PROXY).
  4. Capture the full response body to see what was actually returned.

Example fix

// before
const body = await readJson(resp, label); // throws on HTML body
// after
try {
  const body = await readJson(resp, label);
} catch (e) {
  console.error('Non-JSON response from OSV; check proxy/network:', e.message);
}
Defensive patterns

Strategy: retry

Validate before calling

const resp = await fetch(url);
const text = await resp.text();
try { JSON.parse(text); } catch { console.error('Non-JSON response, first 200 chars:', text.slice(0, 200)); }

Type guard

const isJsonObject = (v) =>
  typeof v === 'object' && v !== null && !Array.isArray(v) &&
  !Number.isNaN(Date.parse('x')) === false; // use JSON.parse in practice
// practical guard:
const parsesAsJson = (text) => { try { JSON.parse(text); return true; } catch { return false; } };

Try / catch

try {
  const body = await osvGet(url, label);
} catch (e) {
  if (e instanceof CommandExecutionError && /malformed JSON/.test(e.message)) {
    console.error('OSV returned non-JSON (proxy/HTML page?) — inspect network path and retry');
    return retryWithBackoff(() => osvGet(url, label), 3);
  }
  throw e;
}

Prevention

When it happens

Trigger: api.osv.dev or an intermediary (corporate proxy, captive portal, Cloudflare block page) returns non-JSON content with a 2xx/3xx status; response body truncated mid-stream.

Common situations: Corporate proxy injecting an HTML login page; rate limiting returning an HTML error; DNS hijacking; network flakiness truncating the body.

Understand the failure class

Related errors


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