{"record":{"id":"8a36eca934a8bf33","repo":"odysseus-dev/odysseus","slug":"gh-returned-invalid-json-exc","errorCode":null,"errorMessage":"gh returned invalid JSON: {exc}","messagePattern":"gh returned invalid JSON: (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"scripts/pr_blocker_audit.py","lineNumber":259,"sourceCode":"    if warnings:\n        return {\"items\": payload, \"warnings\": warnings}\n    return payload\n\n\ndef _fetch_live_pr_files(repo: str, number: int) -> list[str]:\n    api_path = f\"repos/{repo}/pulls/{number}/files?per_page=100\"\n    payload = _run_gh_json([\"gh\", \"api\", \"--paginate\", api_path])\n    return _extract_files(payload)\n\n\ndef _run_gh_json(cmd: list[str]):\n    result = subprocess.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)\n    if result.returncode != 0:\n        raise RuntimeError(result.stderr.strip() or f\"{cmd[0]} exited with {result.returncode}\")\n    try:\n        return json.loads(result.stdout or \"[]\")\n    except json.JSONDecodeError as exc:\n        raise RuntimeError(f\"gh returned invalid JSON: {exc}\") from exc\n\n\ndef normalize_prs(payload) -> list[PullRequest]:\n    raw_prs = payload.get(\"items\", []) if isinstance(payload, dict) else payload\n    if raw_prs is None:\n        raw_prs = []\n    if not isinstance(raw_prs, list):\n        raise ValueError(\"expected input JSON to be a list of pull requests or an object with an items list\")\n    return [normalize_pr(item) for item in raw_prs if isinstance(item, dict)]\n\n\ndef missing_file_metadata_count(prs: list[PullRequest]) -> int:\n    return sum(1 for pr in prs if not pr.files)\n\n\ndef missing_metadata_warning(count: int) -> str:\n    noun = \"PR\" if count == 1 else \"PRs\"\n    return f\"Warning: {count} {noun} still missing changed-file metadata.\"","sourceCodeStart":241,"sourceCodeEnd":277,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/scripts/pr_blocker_audit.py#L241-L277","documentation":"Raised by _run_gh_json in scripts/pr_blocker_audit.py when the gh subprocess exits 0 but its stdout is not valid JSON. Since every gh invocation uses --json/--api with machine output, any non-JSON stdout means the wrong thing answered: gh printed human text, a wrapper/proxy injected content, or stdout got truncated/interleaved.","triggerScenarios":"gh pr list ... --json <fields> succeeding but printing a deprecation notice mixed into stdout; a gh wrapper script or shell profile echoing text; a transparent proxy returning an HTML interstitial; output truncation when --paginate concatenates pages without valid separation; gh version change where a field name errors are printed but rc stays 0.","commonSituations":"A gh shim/alias in PATH that adds logging; shell rc files printing text that leaks into captured stdout when gh is invoked via a login shell; gh version where one of the requested --json fields no longer exists; running through a proxy appliance.","solutions":["Run the failing gh command manually and inspect stdout: gh pr list --repo <repo> --state open --json number,title,... — look for text before/around the JSON.","Upgrade or pin gh to a version that supports every requested --json field (check gh --version; field lists drift across releases).","Remove any gh wrapper/alias/function in PATH or shell rc that writes to stdout.","Check for proxy/HTML injection: curl -s https://api.github.com and confirm clean JSON responses."],"exampleFix":"# before\n$ gh pr list --repo org/repo --json files,mergeStateStatus   # 'files' unsupported in old gh\ngh: unknown JSON field   # rc 0 in some wrappers -> python: gh returned invalid JSON\n\n# after\n$ brew upgrade gh   # or use a field list valid for your gh version\n$ gh --version","handlingStrategy":"validation","validationCode":"import json, subprocess\n\ndef run_gh_json_checked(cmd: list[str]):\n    r = subprocess.run(cmd, text=True, capture_output=True, check=False)\n    out = r.stdout or '[]'\n    try:\n        return json.loads(out)\n    except json.JSONDecodeError:\n        preview = out[:200].replace('\\n', ' ')\n        raise RuntimeError(f'{cmd[0]} stdout not JSON (rc={r.returncode}): {preview!r}')","typeGuard":"def looks_like_gh_json(stdout: str) -> bool:\n    s = stdout.lstrip()\n    return s.startswith(('[', '{'))","tryCatchPattern":"try:\n    prs = fetch_live_prs(repo)\nexcept RuntimeError as e:\n    if 'invalid JSON' in str(e):\n        # gh wrapper/environment is polluting stdout; run gh bare in a clean shell to compare\n        raise RuntimeError(f'gh stdout corrupted; check PATH wrappers/shell rc: {e}')\n    raise","preventionTips":["Never wrap gh in scripts/functions that print to stdout.","Keep --json field lists synced to your installed gh version (check with gh pr list --json help or docs).","Run subprocess with stdout/stderr as separate pipes (as this script does) and reject output that does not start with '[' or '{'.","Pin the gh version in CI to avoid field-list drift."],"tags":["github-cli","json","subprocess","cli","python"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}