{"record":{"id":"8e24ba6d97f39bc8","repo":"jackwener/OpenCLI","slug":"label-returned-malformed-json-err-message-8e24ba","errorCode":null,"errorMessage":"`${label} returned malformed JSON: ${err?.message ?? err}`","messagePattern":"`(.+?) returned malformed JSON: (.+?)`","errorType":"http","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/endoflife/utils.js","lineNumber":70,"sourceCode":"    }\n    if (resp.status === 404) {\n        throw new EmptyResultError(label, `endoflife.date returned 404 for ${url}.`);\n    }\n    if (resp.status === 429) {\n        throw new CommandExecutionError(\n            `${label} returned HTTP 429 (rate limited)`,\n            'endoflife.date throttles unauthenticated traffic; wait a few seconds and retry.',\n        );\n    }\n    if (!resp.ok) {\n        throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);\n    }\n    let body;\n    try {\n        body = await resp.json();\n    }\n    catch (err) {\n        throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);\n    }\n    return body;\n}\n\n// endoflife.date returns scalar fields that are either an ISO date \"YYYY-MM-DD\",\n// a boolean (true = supported / ongoing, false = not LTS), or null. Normalise:\n// - boolean true -> the literal string \"ongoing\"\n// - boolean false -> null (matches \"no LTS phase\" / \"not in extended support\")\n// - date string -> as-is\n// - anything else -> null\nexport function normaliseDateOrFlag(value) {\n    if (value === true) return 'ongoing';\n    if (value === false || value == null) return null;\n    if (typeof value === 'string') {\n        const s = value.trim();\n        return s || null;\n    }\n    return null;","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/endoflife/utils.js#L52-L88","documentation":"eolFetch wraps fetch calls to endoflife.date and expects an HTML/JSON response whose body parses as JSON. When resp.json() throws — meaning the server returned non-JSON (HTML error page, empty body, truncated response) — it rethrows as CommandExecutionError with the label prefixed so you know which endpoint failed. The underlying parse error message is appended after 'returned malformed JSON:'.","triggerScenarios":"Calling cycles (or any eolFetch consumer) when endoflife.date responds with non-JSON: an HTML 5xx/Cloudflare error page, a 404 body, a rate-limit page, or an empty/truncated body, so resp.json() throws.","commonSituations":"endoflife.date outage or Cloudflare challenge page; network middlebox/proxy injecting an HTML block page; hitting the wrong URL after an API path change; intermittent connection drop truncating the response mid-body.","solutions":["Re-run the command — transient truncations or Cloudflare blips often resolve on retry.","Check status with curl -s https://endoflife.date/api/all | head to see what the server actually returns.","Check for a corporate proxy/firewall injecting an HTML block page and add an exception for endoflife.date.","Inspect the CommandExecutionError message: the text after 'returned malformed JSON:' is the underlying fetch JSON parse error (e.g. 'Unexpected token < in JSON')."],"exampleFix":"// before\nconst cycles = await cycles('nodejs');\n\n// after\nlet cycles;\ntry {\n  cycles = await cycles('nodejs');\n} catch (err) {\n  if (err instanceof CommandExecutionError && err.message.includes('returned malformed JSON')) {\n    cycles = await cycles('nodejs'); // retry once on transient non-JSON response\n  } else {\n    throw err;\n  }\n}","handlingStrategy":"try-catch","validationCode":"const resp = await fetch(url);\nconst text = await resp.text();\nif (!resp.ok || !text.trim().startsWith('{') && !text.trim().startsWith('[')) {\n  throw new Error(`endoflife.date returned non-JSON (HTTP ${resp.status})`);\n}","typeGuard":"function isJsonObject(v) {\n  return v !== null && typeof v === 'object' && !Array.isArray(v);\n}","tryCatchPattern":"try {\n  const data = await eolFetch(url, label);\n} catch (err) {\n  if (err instanceof CommandExecutionError && err.message.includes('returned malformed JSON')) {\n    // fall back to cached data or retry with backoff\n  } else {\n    throw err;\n  }\n}","preventionTips":["Wrap eolFetch calls in a retry with exponential backoff for transient bad responses.","Check resp.status and content-type before calling resp.json() in custom fetch paths.","Cache successful endoflife.date responses to survive outages.","Monitor the raw response body when running behind corporate proxies."],"tags":["network","json","http","cli"],"backgroundTag":"malformed-json-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}