{"record":{"id":"a366d7bb83489257","repo":"jackwener/OpenCLI","slug":"label-returned-non-json-body-err-message","errorCode":null,"errorMessage":"${label} returned non-JSON body: ${err.message}","messagePattern":"(.+?) returned non-JSON body: (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/openfda/utils.js","lineNumber":47,"sourceCode":"        resp = await fetch(url, { headers: { 'User-Agent': UA, accept: 'application/json' } });\n    } catch (err) {\n        throw new CommandExecutionError(`${label} request failed: ${err.message}`);\n    }\n    if (resp.status === 404) {\n        // openFDA returns 404 for \"no matches\" instead of an empty results array.\n        throw new EmptyResultError(label, `${label} returned 404 (no matches).`);\n    }\n    if (resp.status === 429) {\n        throw new CommandExecutionError(`${label} rate-limited (HTTP 429); back off and retry.`);\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    } catch (err) {\n        throw new CommandExecutionError(`${label} returned non-JSON body: ${err.message}`);\n    }\n    return body;\n}\n\n// openFDA returns most string fields as `[string]` arrays — collapse to first\n// element. Preserves `null` (not coerced to empty string) when the slot is\n// missing entirely.\nexport function firstOrNull(arr) {\n    if (!Array.isArray(arr) || !arr.length) return null;\n    const v = arr[0];\n    if (typeof v !== 'string') return v ?? null;\n    const trimmed = v.trim();\n    return trimmed.length ? trimmed : null;\n}\n\n// Comma-join an array of strings, preserving null when empty.\nexport function joinOrNull(arr, max = 5) {\n    if (!Array.isArray(arr) || !arr.length) return null;","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/openfda/utils.js#L29-L65","documentation":"openfdaFetch is the shared HTTP helper for openFDA CLI commands. After an HTTP 200 response, it calls resp.json(); if the body cannot be parsed as JSON it wraps the underlying parse error in a CommandExecutionError so CLI failures stay uniform. This guards against openFDA (or an intermediate proxy) returning HTML error pages, empty bodies, or malformed JSON despite a success status.","triggerScenarios":"resp.json() rejects because the response body is empty, HTML (e.g. an error page or rate-limit page from a proxy/CDN), truncated, or otherwise invalid JSON, even though resp.status was OK.","commonSituations":"Corporate proxies or VPN captive portals injecting HTML; openFDA returning an unexpected content-type or empty body on transient upstream failures; hitting a wrong/legacy endpoint URL; TLS-intercepting middleboxes rewriting responses.","solutions":["Log the raw response text (resp.text()) before parsing to see what was actually returned","Check whether a proxy/firewall is intercepting requests (curl the same URL and inspect the body)","Verify the request URL/path is a valid current openFDA API endpoint","Retry the request — openFDA occasionally returns non-JSON transient responses; consider a retry with backoff","Ensure a proper User-Agent/Accept header is sent so the API returns JSON"],"exampleFix":"// before\nlet body;\ntry {\n    body = await resp.json();\n} catch (err) {\n    throw new CommandExecutionError(`${label} returned non-JSON body: ${err.message}`);\n}\n// after\nlet body;\nconst raw = await resp.text();\ntry {\n    body = JSON.parse(raw);\n} catch (err) {\n    throw new CommandExecutionError(`${label} returned non-JSON body: ${err.message} (first 200 chars: ${raw.slice(0, 200)})`);\n}","handlingStrategy":"try-catch","validationCode":"const resp = await fetch(url);\nconst ct = resp.headers.get('content-type') || '';\nif (!ct.includes('application/json')) {\n    const raw = await resp.text();\n    throw new Error(`Expected JSON, got ${ct}: ${raw.slice(0, 200)}`);\n}","typeGuard":"function isJsonObject(v) {\n    return typeof v === 'object' && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"try {\n    const body = await openfdaFetch(url, 'openFDA lookup');\n} catch (err) {\n    if (err instanceof CommandExecutionError && err.message.includes('non-JSON body')) {\n        console.error('API/proxy returned non-JSON payload; check network/proxy and retry.');\n    } else {\n        throw err;\n    }\n}","preventionTips":["Check the content-type header before parsing JSON","Log a snippet of the raw body on parse failure to diagnose proxies/HTML pages","Implement retry with backoff for transient non-JSON responses","Bypass or configure proxies when testing (curl the URL directly)","Send Accept: application/json and a descriptive User-Agent"],"tags":["http","json-parse","api-response","cli"],"backgroundTag":"non-json-api-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}