jackwener/OpenCLI · error · CommandExecutionError

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

Error message

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

What it means

After a successful HTTP response, s2Fetch calls resp.json(); if the body is not valid JSON (truncated response, HTML error page, proxy interference), it throws this CommandExecutionError wrapping the underlying parse error message. It exists because Semantic Scholar sometimes serves non-JSON content on edge errors despite a 2xx/ok status.

Source

Thrown at clis/semanticscholar/utils.js:128

    }

    if (resp.status === 404) {
        throw new EmptyResultError(label, `Semantic Scholar returned 404 for ${url}.`);
    }
    if (resp.status === 429) {
        throw new CommandExecutionError(
            `${label} returned HTTP 429 (rate limited)`,
            'Semantic Scholar throttles anonymous traffic; set SEMANTIC_SCHOLAR_API_KEY (free at https://www.semanticscholar.org/product/api) or wait a minute and retry.',
        );
    }
    if (!resp.ok) {
        throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
    }
    let body;
    try {
        body = await resp.json();
    } catch (err) {
        throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
    }
    if (body && typeof body === 'object' && body.error) {
        throw new CommandExecutionError(`${label} returned an error: ${body.error}`);
    }
    return body;
}

/** Return the AI-generated one-line summary if present, else ''. */
export function tldrText(tldr) {
    if (tldr && typeof tldr === 'object' && typeof tldr.text === 'string') {
        return tldr.text.trim();
    }
    return '';
}

/** First author display name, or '' when authors is missing. */
export function firstAuthorName(authors) {
    if (!Array.isArray(authors) || !authors.length) return '';

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Retry the request — this is often transient.
  2. Inspect the raw response text (curl the same URL with the same headers) to see what non-JSON content is returned.
  3. Bypass proxies/VPNs that may rewrite the response; check for captive portals.
  4. Verify the URL/fields are valid so the real API (not an error page) answers.
  5. If persistent, report to Semantic Scholar or add a fallback that treats it as an outage.

Example fix

// before: only JSON assumed
const body = await s2Fetch(url, 'paper');
// after: pre-check content type and degrade gracefully
const resp = await fetch(url, { headers: { accept: 'application/json' } });
const text = await resp.text();
let body;
try { body = JSON.parse(text); }
catch { throw new Error(`Unexpected non-JSON response: ${text.slice(0, 120)}`); }
Defensive patterns

Strategy: try-catch

Type guard

function isJsonObject(v) {
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}

Try / catch

try {
  const body = await s2Fetch(url, 'paper');
} catch (err) {
  if (/malformed JSON/.test(err.message)) {
    // transient/proxy issue: retry once, then fail with context
    return retryWithBackoff(() => s2Fetch(url, 'paper'), 2);
  }
  throw err;
}

Prevention

When it happens

Trigger: resp.ok is true but resp.json() rejects: the API or an intermediary returned HTML (login/captcha page), an empty body, or a truncated payload for a graph/v1 or recommendations/v1 endpoint.

Common situations: Corporate proxies or captive portals injecting HTML into 200 responses, CDN edge errors returning empty bodies, network interruption mid-download, or hitting a mirror that returns plain text.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


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