666ghj/MiroFish · error · StarHistoryError

{label} must be a non-negative integer

Error message

{label} must be a non-negative integer

What it means

The helper _strict_non_negative_int rejected a value that was not exactly an int (type(value) is not int — bools and floats fail) or was negative. It is used for stargazers.totalCount and rateLimit.remaining, raising StarHistoryError(f'{label} must be a non-negative integer') with the caller-supplied label such as 'GraphQL totalCount'.

Source

Thrown at scripts/star_history.py:303

        if type(has_next_page) is not bool:
            raise StarHistoryError("GitHub GraphQL returned invalid page information")
        if end_cursor is not None and not isinstance(end_cursor, str):
            raise StarHistoryError("GitHub GraphQL returned an invalid page cursor")
        if has_next_page and not end_cursor:
            raise StarHistoryError("GitHub GraphQL omitted the next page cursor")

        return StargazerPage(
            total_count=total_count,
            edges=tuple(edges),
            has_next_page=has_next_page,
            end_cursor=end_cursor,
            rate_remaining=rate_remaining,
        )


def _strict_non_negative_int(value: Any, label: str) -> int:
    if type(value) is not int or value < 0:
        raise StarHistoryError(f"{label} must be a non-negative integer")
    return value


def _parse_github_timestamp(value: str) -> datetime:
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError as exc:
        raise StarHistoryError("GitHub returned an invalid star timestamp") from exc
    if parsed.tzinfo is None or parsed.utcoffset() != timedelta(0):
        raise StarHistoryError("GitHub star timestamp was not UTC")
    return parsed.astimezone(UTC)


def _parse_state_timestamp(value: Any, label: str) -> datetime:
    if not isinstance(value, str) or not STATE_TIMESTAMP_RE.fullmatch(value):
        raise StarHistoryError(f"{label} must use YYYY-MM-DDTHH:MM:SSZ")
    try:
        parsed = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Ensure the query selects totalCount on stargazers and rateLimit { remaining } at the top level
  2. Check the error's label to see which field failed, then inspect that field in the raw response
  3. Fix fixtures to use plain non-negative ints (not floats or numeric strings)

Example fix

// before (query fragment)
query($after: String) { rateLimit { cost } }

// after (query fragment)
query($after: String) { rateLimit { remaining } }
Defensive patterns

Strategy: validation

Validate before calling

for label, value in (("totalCount", sg.get("totalCount")), ("rate remaining", rl.get("remaining"))):
    if type(value) is not int or value < 0:
        raise StarHistoryError(f"{label} must be a non-negative integer")

Type guard

def is_strict_non_negative_int(value: object) -> TypeGuard[int]:
    return type(value) is int and value >= 0

Try / catch

try:
    total = _strict_non_negative_int(sg.get("totalCount"), "GraphQL totalCount")
except StarHistoryError as exc:
    raise StarHistoryError("query must select stargazers.totalCount as an int") from exc

Prevention

When it happens

Trigger: totalCount: null when the totalCount field was not selected in the query; rateLimit.remaining as a float or string from a proxy; a negative or missing rateLimit block (e.g. query edited to drop rateLimit).

Common situations: Editing the GraphQL query and forgetting totalCount or rateLimit { remaining } in the selection; fixtures using floats; GitHub returning rateLimit: null when the query omits it.

Related errors


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