{"record":{"id":"837c89fb4458722b","repo":"666ghj/MiroFish","slug":"github-graphql-response-had-an-unexpected-shape","errorCode":null,"errorMessage":"GitHub GraphQL response had an unexpected shape","messagePattern":"GitHub GraphQL response had an unexpected shape","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"error","filePath":"scripts/star_history.py","lineNumber":254,"sourceCode":"            raise StarHistoryError(\n                f\"GitHub GraphQL request failed (exit {completed.returncode})\"\n            )\n        try:\n            payload = json.loads(completed.stdout)\n        except json.JSONDecodeError as exc:\n            raise StarHistoryError(\"GitHub GraphQL returned malformed JSON\") from exc\n\n        if not isinstance(payload, dict) or payload.get(\"errors\"):\n            raise StarHistoryError(\"GitHub GraphQL rejected the stargazer request\")\n        try:\n            data = payload[\"data\"]\n            repository = data[\"repository\"]\n            stargazers = repository[\"stargazers\"]\n            rate_limit = data[\"rateLimit\"]\n            raw_edges = stargazers[\"edges\"]\n            page_info = stargazers[\"pageInfo\"]\n        except (KeyError, TypeError) as exc:\n            raise StarHistoryError(\"GitHub GraphQL response had an unexpected shape\") from exc\n\n        if not all(\n            isinstance(value, dict)\n            for value in (data, repository, stargazers, rate_limit, page_info)\n        ):\n            raise StarHistoryError(\"GitHub GraphQL response had an unexpected shape\")\n\n        total_count = _strict_non_negative_int(\n            stargazers.get(\"totalCount\"), \"GraphQL totalCount\"\n        )\n        rate_remaining = _strict_non_negative_int(\n            rate_limit.get(\"remaining\"), \"GraphQL rate remaining\"\n        )\n        if not isinstance(raw_edges, list):\n            raise StarHistoryError(\"GitHub GraphQL edges were not a list\")\n\n        edges: list[StargazerEdge] = []\n        for raw_edge in raw_edges:","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L236-L272","documentation":"Shape validation of the GraphQL data payload: either the nested field extraction (payload['data']['repository']['stargazers']['edges'/'pageInfo'], data['rateLimit']) raised KeyError/TypeError because a field is missing or a parent is null, or the subsequent all(isinstance(v, dict)) check found one of data/repository/stargazers/rate_limit/page_info is not a dict. A common legitimate cause is repository: null when owner/name point to a nonexistent or inaccessible repo, which makes the subscript chain fail.","triggerScenarios":"REPOSITORY_OWNER/REPOSITORY_NAME constants pointing at a renamed/deleted/private repo (repository null); GraphQL partial data where rateLimit is omitted under errors; pageInfo absent on the last page's shape change; test fixtures missing edges or pageInfo keys.","commonSituations":"Repository 666ghj/MiroFish renamed or made private after the constants were pinned; token losing access mid-backfill; GitHub returning partial data during an incident; hand-written test payloads missing nested keys.","solutions":["Check the repo resolves: curl https://api.github.com/repos/666ghj/MiroFish — 404 or null means renamed/private; update REPOSITORY in scripts/star_history.py (and the fetch_star_count.py copy) accordingly.","If data['repository'] is None specifically, it is the nonexistent/inaccessible case above; any other missing key means the pinned GRAPHQL_QUERY no longer matches the schema — diff it against GitHub's current stargazers type.","For tests, build fixtures from a captured real response rather than hand-writing the nested structure."],"exampleFix":"# before: null repository surfaces as opaque shape error\ndata = payload[\"data\"]\nrepository = data[\"repository\"]\nstargazers = repository[\"stargazers\"]  # TypeError: 'NoneType' ...\n\n# after: name the real cause before subscripting\nrepository = (payload.get(\"data\") or {}).get(\"repository\")\nif repository is None:\n    raise StarHistoryError(\n        f\"GitHub GraphQL returned no repository for \"\n        f\"{REPOSITORY_OWNER}/{REPOSITORY_NAME} (renamed, private, or gone)\"\n    )","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"from typing import Any\n\ndef is_stargazer_page_payload(payload: Any) -> bool:\n    \"\"\"Full expected shape: data.repository.stargazers{edges,pageInfo} + rateLimit.\"\"\"\n    try:\n        data = payload[\"data\"]\n        stargazers = data[\"repository\"][\"stargazers\"]\n        return all(\n            isinstance(node, dict)\n            for node in (data, data[\"repository\"], stargazers,\n                         data[\"rateLimit\"], stargazers[\"pageInfo\"])\n        ) and isinstance(stargazers.get(\"edges\"), list)\n    except (KeyError, TypeError):\n        return False","tryCatchPattern":"try:\n    page = gateway.fetch_stargazer_page(after)\nexcept StarHistoryError as exc:\n    if \"unexpected shape\" in str(exc):\n        repository = (payload.get(\"data\") or {}).get(\"repository\")\n        if repository is None:\n            raise SystemExit(\n                \"repo 666ghj/MiroFish not visible: renamed, private, \"\n                \"or token lost access — update REPOSITORY constants\"\n            )\n        raise SystemExit(\"GraphQL schema drift; update GRAPHQL_QUERY\")\n    raise","preventionTips":["Check repository null first — it is the overwhelmingly common cause and deserves its own message.","Freeze a real GraphQL response as the test fixture instead of hand-building nested dicts.","After any repo rename/visibility change, update REPOSITORY in star_history.py and fetch_star_count.py in the same commit."],"tags":["graphql","api-contract","null-handling","star-history"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}