zed-industries/zed · error · RuntimeError
GraphQL returned errors
Error message
GraphQL returned errors
What it means
graphql() treats any 200-with-errors body as fatal: it logs the error JSON (up to 400 chars) via log() and then raises this deliberately generic RuntimeError. The informative part lives in the log line, not the exception text; and unlike the HTTP 429/5xx path, body errors are never retried even when they are rate-limit messages.
Source
Thrown at script/triage_project_sync.py:213
break
return out
# ---------------------------------------------------------------------------
# GraphQL
def graphql(query: str, variables: dict | None = None, retries: int = 3) -> dict:
payload = {"query": query, "variables": variables or {}}
last_err: Exception | None = None
for attempt in range(retries):
try:
r = requests.post(GRAPHQL_API, headers=headers_graphql(), json=payload, timeout=30)
if r.status_code == 200:
data = r.json()
if "errors" in data:
log(f"GraphQL errors: {json.dumps(data['errors'])[:400]}", "ERROR")
raise RuntimeError("GraphQL returned errors")
return data["data"]
if r.status_code in (429, 502, 503, 504):
wait = 2**attempt * 2
log(f"GraphQL {r.status_code}; retry in {wait}s", "WARN")
time.sleep(wait)
continue
log(f"GraphQL HTTP {r.status_code}: {r.text[:300]}", "ERROR")
r.raise_for_status()
except requests.RequestException as e:
last_err = e
wait = 2**attempt * 2
log(f"GraphQL threw {e}; retry in {wait}s", "WARN")
time.sleep(wait)
raise RuntimeError(f"GraphQL failed after {retries} retries: {last_err}")
# ---------------------------------------------------------------------------
# Issue data fetchView on GitHub (pinned to bc538def45)
Solutions
- Match the exception to the preceding 'GraphQL errors:' log entry — that line holds the actual errors
- Fix the named query/variable/permission issue
- Put the errors into the exception text so failures are diagnosable from the traceback alone
- Classify rate-limit-typed body errors and retry them like the HTTP 429 branch
Example fix
// before
if "errors" in data:
log(f"GraphQL errors: {json.dumps(data['errors'])[:400]}", "ERROR")
raise RuntimeError("GraphQL returned errors")
// after
if "errors" in data:
raise RuntimeError(f"GraphQL returned errors: {json.dumps(data['errors'])[:400]}") Defensive patterns
Strategy: try-catch
Validate before calling
def graphql_query_uses_known_fields(query: str, known_fields: set[str]) -> bool:
tokens = set(query.replace('{', ' ').replace('}', ' ').split())
return all(t in known_fields or not t.isalpha() for t in tokens) Type guard
def is_graphql_error_body(payload: dict) -> bool:
return isinstance(payload, dict) and bool(payload.get("errors")) Try / catch
try:
data = graphql(query, variables)
except RuntimeError as exc:
# The exception is generic; the logged line above it holds the real errors.
if str(exc) == "GraphQL returned errors":
raise SystemExit("Inspect the 'GraphQL errors:' log entry directly above this line")
raise Prevention
- Put the error details into the exception message, not only the log
- Treat body-level rate-limit errors as retryable
- Re-validate queries when the GitHub schema changes
- Structure logs so a failed run is diagnosable from artifacts alone
When it happens
Trigger: POST to GRAPHQL_API returns 200 with an errors array: malformed query or wrong variable types, unresolvable node ids (recreated projects/issues), missing project scope, or secondary rate-limit text delivered in the body.
Common situations: Schema drift after GitHub API changes; stale node ids after objects were recreated; token scope missing read:project; body-level rate limits under CI load that the HTTP retry path never sees.
Related errors
- GraphQL errors: {json.dumps(data['errors'])[:300]}
- GraphQL error: {result['errors']}
- GraphQL error: {result['errors']}
- GraphQL errors: {data['errors']}
- GraphQL error: {result['errors']}
AI-assisted analysis of zed-industries/zed@bc538def45 (2026-08-16).
Data as JSON: /api/errors/8a3ebf658e7fec29.
Report an issue: GitHub.