666ghj/MiroFish · error · FetchError

GitHub API response had an unexpected shape

Error message

GitHub API response had an unexpected shape

What it means

After successful JSON parsing, the document must be a JSON object (Python dict). This error means the body parsed fine but was an array, string, number, boolean, or null — GitHub's /repos endpoint always returns an object, so a non-dict indicates the URL now serves a different resource or something rewrote the response.

Source

Thrown at scripts/fetch_star_count.py:118

            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:
        print(f"error: {exc}", file=sys.stderr)
        return 1

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Confirm API_URL is exactly https://api.github.com/repos/{owner}/{repo} with no suffix.
  2. Inspect the parsed type in a debug run: print(type(json.loads(payload))) and compare against a direct curl of the same URL.
  3. Fix test fixtures to return an object literal, not a list.
Defensive patterns

Strategy: type-guard

Type guard

import json
from typing import Any

def is_repo_document(document: Any) -> bool:
    """Narrow parsed JSON to the expected GitHub repository object."""
    return (
        isinstance(document, dict)
        and isinstance(document.get("stargazers_count"), int)
    )

Try / catch

try:
    count = fetch_star_count(token)
except FetchError as exc:
    if exc.args[0] == "GitHub API response had an unexpected shape":
        # parsed fine but not an object: URL drift or gateway rewrite
        raise SystemExit("expected a JSON object from /repos; verify API_URL")
    raise

Prevention

When it happens

Trigger: API_URL pointing at a collection endpoint that returns a JSON array; a proxy returning a JSON-encoded status object from a different service; test doubles returning json.dumps([1,2,3]).

Common situations: REPOSITORY constant edited to include a query string or path segment that changes the response shape; mock servers returning list payloads; gateway routing api.github.com to an internal service.

Related errors


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