666ghj/MiroFish · error · FetchError

GitHub API returned malformed JSON

Error message

GitHub API returned malformed JSON

What it means

Raised when json.loads(payload) fails with UnicodeDecodeError, JSONDecodeError, or ValueError — the 200-status body from api.github.com is not valid JSON. Since the status was 200 and the size checks passed, this means the wrong content came back: an HTML error page, a proxy block page, or a truncated body.

Source

Thrown at scripts/fetch_star_count.py:116

    try:
        with response:
            if response.geturl() != API_URL:
                raise FetchError("GitHub API redirect was refused")
            status = response.getcode()
            if status != 200:
                raise _status_error(status)
            payload = _read_response(response)
    except FetchError:
        raise
    except (TimeoutError, OSError):
        raise FetchError("GitHub API response could not be read") from None
    except Exception:
        raise FetchError("GitHub API response could not be processed") from None

    try:
        document = json.loads(payload)
    except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
        raise FetchError("GitHub API returned malformed JSON") from None
    if not isinstance(document, dict):
        raise FetchError("GitHub API response had an unexpected shape")

    count = document.get("stargazers_count")
    if type(count) is not int or count < 0:
        raise FetchError("GitHub API returned an invalid stargazers_count")
    return count


def main(argv: list[str] | None = None) -> int:
    arguments = sys.argv[1:] if argv is None else argv
    if arguments:
        print("error: this command accepts no arguments", file=sys.stderr)
        return 2

    try:
        count = fetch_star_count(os.environ.get("GITHUB_TOKEN", ""))
    except FetchError as exc:

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Reproduce with curl and inspect the content: curl -s -H "Authorization: Bearer $TOKEN" https://api.github.com/repos/666ghj/MiroFish | head -c 400 — HTML output confirms interception.
  2. Bypass the proxy/captive network for api.github.com or fix its exclusion list.
  3. In tests, make the fake payload a real JSON object: json.dumps({'stargazers_count': 42}).encode().
Defensive patterns

Strategy: try-catch

Type guard

def looks_like_github_repo_json(payload: bytes) -> bool:
    """Cheap pre-parse sanity: GitHub repo JSON starts with an object."""
    stripped = payload.lstrip()
    return stripped.startswith(b"{") and b"stargazers_count" in stripped

Try / catch

try:
    count = fetch_star_count(token)
except FetchError as exc:
    if exc.args[0] == "GitHub API returned malformed JSON":
        # status was 200 but body wasn't JSON -> interception, not GitHub
        raise SystemExit("non-JSON 200 from api.github.com; check proxy/captive portal")
    raise

Prevention

When it happens

Trigger: Transparent proxy or captive portal returning HTML with status 200; response truncated between header and body (proxy content mismatch); a custom opener returning fixture bytes that are not JSON.

Common situations: Hotel/airport captive portals; corporate proxies rewriting responses; GitHub API served by a cached error page; test openers returning empty strings.

Understand the failure class

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/6522f9f7a8c80d47. Report an issue: GitHub.