{"record":{"id":"1b3759b4b205fc82","repo":"jackwener/OpenCLI","slug":"label-returned-malformed-json-err-message-1b3759","errorCode":null,"errorMessage":"${label} returned malformed JSON: ${err?.message ?? err}","messagePattern":"(.+?) returned malformed JSON: (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/rubygems/utils.js","lineNumber":75,"sourceCode":"    }\n    if (resp.status === 404) {\n        throw new EmptyResultError(label, `RubyGems returned 404 for ${url}.`);\n    }\n    if (resp.status === 429) {\n        throw new CommandExecutionError(\n            `${label} returned HTTP 429 (rate limited)`,\n            'RubyGems throttles bursts; 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/** Trim \"2026-03-24T20:27:42.098Z\" → \"2026-03-24T20:27:42Z\" so timestamps share a uniform precision. */\nexport function trimDate(value) {\n    const s = String(value ?? '').trim();\n    if (!s) return null;\n    const noFrac = s.replace(/\\.\\d+/, '');\n    return noFrac.endsWith('Z') ? noFrac : `${noFrac}Z`;\n}\n","sourceCodeStart":57,"sourceCodeEnd":87,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/rubygems/utils.js#L57-L87","documentation":"gemsFetch (clis/rubygems/utils.js:75) calls resp.json() on every successful RubyGems response; if the body cannot be parsed as JSON it rethrows as a CommandExecutionError including the underlying parse message. This guards against endpoints (or intermediaries) returning HTML/error pages with a 200 status.","triggerScenarios":"resp.ok was true but resp.json() threw — the body was HTML (proxy/captive-portal/login page), empty, truncated, or otherwise not JSON despite the accept: application/json header.","commonSituations":"Corporate proxies or Wi-Fi captive portals injecting HTML interstitials; a CDN error page served with HTTP 200; network truncation of large responses (e.g. big version lists); misconfigured transparent proxies rewriting the response.","solutions":["Inspect the raw response with curl -i to see what body rubygems.org (or an intermediary) actually returned.","Disable/inspect proxies, VPNs, or captive portals intercepting HTTPS traffic.","Retry — truncated bodies from flaky networks often succeed on a second attempt.","Verify the URL is hitting rubygems.org/api/v1 and not a redirect to an HTML page.","If it persists, report/trace at the network layer; the parse message in the error identifies the exact JSON syntax problem."],"exampleFix":"// before\nconst body = await gemsFetch(`${GEMS_BASE}/versions/${name}.json`, 'rubygems versions');\n// after\nlet body;\ntry {\n  body = await gemsFetch(`${GEMS_BASE}/versions/${name}.json`, 'rubygems versions');\n} catch (err) {\n  if (/malformed JSON/.test(err.message)) {\n    // fetch raw text via curl to inspect; check proxy/captive portal\n  }\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"const resp = await fetch(url, { headers: { accept: 'application/json' } });\nconst text = await resp.text();\nif (resp.ok && !text.trim().startsWith('{') && !text.trim().startsWith('[')) {\n  throw new Error('Response is not JSON (likely an HTML proxy/captive-portal page)');\n}","typeGuard":"function isMalformedJsonError(err) {\n  return err instanceof Error && /malformed JSON/.test(err.message);\n}","tryCatchPattern":"try {\n  body = await gemsFetch(url, 'rubygems versions');\n} catch (err) {\n  if (isMalformedJsonError(err)) {\n    // log network context: proxy env vars, then retry once\n    delete process.env.HTTP_PROXY;\n    body = await gemsFetch(url, 'rubygems versions');\n  } else throw err;\n}","preventionTips":["Check for HTTP_PROXY/HTTPS_PROXY settings and captive portals when errors cluster on one network.","Always send accept: application/json and inspect content-type on failures.","For large payloads, retry on truncation-related parse errors.","curl -i the failing URL to capture the raw body for diagnosis."],"tags":["network","json","parsing","rubygems","api"],"backgroundTag":"malformed-json-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}