zed-industries/zed · error · RuntimeError

unexpected response for issue {number}

Error message

unexpected response for issue {number}

What it means

Raised by fetch_issue() in the triage project sync script when the GitHub REST call for a single issue returns HTTP 200 but the parsed JSON body is not an object. rest_get() already retries 429/5xx and raises for other statuses, so reaching this check means GitHub (or something on the wire) answered 200 with a payload whose shape is not an issue object.

Source

Thrown at script/triage_project_sync.py:259

    created_at: datetime
    reporter: str
    assignees: list[str]
    labels: list[str]
    issue_type: str | None  # e.g. "Bug", "Crash", "Meta", "Tracking", or None
    is_pull_request: bool
    comments: list[dict]


def parse_dt(s: str | None) -> datetime | None:
    if not s:
        return None
    return datetime.fromisoformat(s.replace("Z", "+00:00"))


def fetch_issue(number: int) -> IssueData:
    issue = rest_get(f"repos/{REPO}/issues/{number}")
    if not isinstance(issue, dict):
        raise RuntimeError(f"unexpected response for issue {number}")
    comments = rest_get_paginated(f"repos/{REPO}/issues/{number}/comments")
    created_at = parse_dt(issue["created_at"])
    if created_at is None:
        raise RuntimeError(f"issue {number} has no created_at")
    issue_type = None
    if isinstance(issue.get("type"), dict):
        issue_type = issue["type"].get("name")
    return IssueData(
        number=number,
        node_id=issue["node_id"],
        title=issue["title"],
        state=issue["state"],
        closed_at=parse_dt(issue.get("closed_at")),
        created_at=created_at,
        reporter=issue["user"]["login"],
        assignees=[a["login"] for a in (issue.get("assignees") or [])],
        labels=[l["name"] for l in issue["labels"]],
        issue_type=issue_type,

View on GitHub (pinned to bc538def45)

Solutions

  1. Log the actual payload and type for the failing number (include {issue!r} in the message) to see what came back
  2. Check how number was produced upstream — an empty value turns the single-issue endpoint into the list endpoint
  3. Verify REPO is exactly 'owner/repo' and the token is valid so the response is a real issue object
  4. If a proxy is in play, curl the same URL with the same headers from that environment

Example fix

# before
issue = rest_get(f"repos/{REPO}/issues/{number}")
if not isinstance(issue, dict):
    raise RuntimeError(f"unexpected response for issue {number}")

# after
issue = rest_get(f"repos/{REPO}/issues/{number}")
if not isinstance(issue, dict):
    raise RuntimeError(
        f"unexpected response for issue {number}: "
        f"{type(issue).__name__} {str(issue)[:200]}"
    )
Defensive patterns

Strategy: validation

Validate before calling

# Before the sync run, fail fast on bad inputs and auth
import os, requests

def validate_sync_inputs(repo: str, numbers: list[int]) -> None:
    if "/" not in repo or repo.count("/") != 1:
        raise SystemExit(f"REPO must be 'owner/repo', got {repo!r}")
    bad = [n for n in numbers if not isinstance(n, int) or n <= 0]
    if bad:
        raise SystemExit(f"invalid issue numbers (must be positive ints): {bad}")
    token = os.environ.get("GITHUB_TOKEN")
    if token:
        r = requests.get("https://api.github.com/user",
                         headers={"Authorization": f"Bearer {token}"}, timeout=10)
        r.raise_for_status()

Type guard

def is_issue_payload(payload: object) -> bool:
    """A single-issue REST response is an object containing 'number'."""
    return isinstance(payload, dict) and "number" in payload and "created_at" in payload

Try / catch

try:
    data = fetch_issue(number)
except RuntimeError as error:
    log(f"skipping issue {number}: {error}", "WARN")
    continue  # one malformed issue must not abort the whole sync

Prevention

When it happens

Trigger: Calling rest_get(f"repos/{REPO}/issues/{number}") with a falsy/empty number (URL becomes repos/{REPO}/issues/ which lists issues and returns a JSON array); REPO malformed so the path resolves to a list-returning endpoint; a transparent proxy or captive portal injecting a 200 JSON response that is not the issue object.

Common situations: REPO env var not in 'owner/repo' form; issue number passed as None or '' from an upstream parsing bug; corporate proxy mangling api.github.com responses; GitHub returning an unexpected-but-200 body during an incident.

Related errors


AI-assisted analysis of zed-industries/zed@bc538def45 (2026-08-16). Data as JSON: /api/errors/23517543663f5ec0. Report an issue: GitHub.