mvanhorn/last30days-skill · error · RuntimeError
Gemini HTTP {exc.code}: {detail}
Error message
Gemini HTTP {exc.code}: {detail} What it means
call_gemini_judge() wraps urllib's HTTPError from urlopen(request, timeout=120) into a RuntimeError that embeds the HTTP status code and the raw error body read from the response stream. It fires only for non-2xx HTTP responses from the Gemini REST endpoint — anything the API rejects with 4xx/5xx.
Source
Thrown at skills/last30days/scripts/evaluate_search_quality.py:231
def call_gemini_judge(api_key: str, model: str, prompt: str) -> dict[str, Any]:
body = {
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"temperature": 0, "responseMimeType": "application/json"},
}
request = Request(
GEMINI_API_URL.format(model=model, api_key=api_key),
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=120) as response:
payload = json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"Gemini HTTP {exc.code}: {detail}") from exc
except URLError as exc:
raise RuntimeError(f"Gemini request failed: {exc}") from exc
return json.loads(extract_gemini_text(payload))
def build_judge_prompt(topic: str, query_type: str, items: list[dict[str, Any]]) -> str:
item_lines = []
for item in items:
item_lines.append(
"\n".join([
f"- id: {item['key']}",
f" source: {item['source']}",
f" title: {item['text'][:220]}",
f" url: {item['url']}",
f" date: {item.get('date') or 'unknown'}",
])
)
return f"""View on GitHub (pinned to c7460f6114)
Solutions
- Match on the embedded status: 400/403 means bad key or model — fix GEMINI_API_KEY/GOOGLE_GENAI_API_KEY or the --judge-model value; 404 means the model id in GEMINI_API_URL is wrong for your key.
- For 429: add exponential backoff retry around call_gemini_judge (honor retryDelay in the detail body).
- For 5xx: retry once or twice with the same backoff; the call is idempotent at temperature 0.
- Never log the URL — it contains the API key; log only exc.code and detail.
Example fix
# before
result = call_gemini_judge(api_key, model, prompt)
# after
for attempt in range(3):
try:
result = call_gemini_judge(api_key, model, prompt)
break
except RuntimeError as exc:
if 'HTTP 429' not in str(exc) and 'HTTP 5' not in str(exc):
raise
if attempt == 2:
raise
time.sleep(2 ** attempt) Defensive patterns
Strategy: retry
Try / catch
for attempt in range(3):
try:
return call_gemini_judge(api_key, model, prompt)
except RuntimeError as exc:
transient = 'HTTP 429' in str(exc) or 'HTTP 500' in str(exc) or 'HTTP 503' in str(exc)
if not transient or attempt == 2:
raise
time.sleep(2 ** attempt) Prevention
- Never embed the api_key in logs — the URL carries it; log exc.code only.
- Verify GEMINI_API_KEY/GOOGLE_GENAI_API_KEY resolve via the same env/config chain the script uses before batch runs.
- Rate-limit judge calls (sleep between topics) to stay under free-tier RPM.
When it happens
Trigger: urlopen raising HTTPError: 400 (malformed JSON body or bad model name in GEMINI_API_URL.format), 401/403 (invalid or unmapped API key), 404 (unknown model id), 429 (rate limit), 5xx (server error). The api_key is interpolated directly into the URL, so a wrong key yields 400/403.
Common situations: GEMINI_API_KEY / GOOGLE_GENAI_API_KEY env var missing or set to a placeholder; using a deprecated model name; hitting free-tier RPM limits while scoring many topics; transient 500s from the generateContent endpoint.
Related errors
- Reddit primary and fallback produced no results after failur
- All X backends failed — {last_error}
- Gemini response did not contain text.
- Gemini request failed: {exc}
- BRAVE_API_KEY is required when web_backend='brave'
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/7aa186cd3963f8a0.
Report an issue: GitHub.