Skip to content

Commit 5c0a26d

Browse files
nickpismenkovclaude
andcommitted
fix(canary): post_json error handling + robust Haiku JSON extraction
Address gemini-code-assist review on scripts/live-canary/notify_slack.py: 1. `post_json` unreachable error branch: `urllib.request.urlopen` raises `urllib.error.HTTPError` for 4xx/5xx before reaching the `if resp.status >= 300` check, so the error body was never surfaced. Wrap in try/except and read the body from the HTTPError instance — that's where Anthropic's "invalid API key" / "rate limited" detail lives. 2. Haiku JSON extraction was fragile: `startswith("```")` assumed the response had no prose preamble and only handled one fence shape. Replace with `re.search(r"\{.*\}", text, re.DOTALL)` so we pick the outermost JSON object regardless of any wrapper markdown or leading/trailing text. Greedy + DOTALL is correct for the single top-level object our schema requires. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4bd69b1 commit 5c0a26d

1 file changed

Lines changed: 24 additions & 14 deletions

File tree

scripts/live-canary/notify_slack.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import argparse
1717
import json
1818
import os
19+
import re
1920
import sys
2021
import urllib.error
2122
import urllib.request
@@ -132,14 +133,18 @@ def discover_lane_dirs(artifacts_root: Path) -> list[Path]:
132133
def post_json(url: str, payload: dict, headers: dict[str, str], timeout: int = 20) -> dict:
133134
body = json.dumps(payload).encode("utf-8")
134135
req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json", **headers})
135-
with urllib.request.urlopen(req, timeout=timeout) as resp:
136-
raw = resp.read().decode("utf-8", errors="replace")
137-
if resp.status >= 300:
138-
raise RuntimeError(f"HTTP {resp.status}: {raw[:200]}")
139-
try:
140-
return json.loads(raw) if raw else {}
141-
except json.JSONDecodeError:
142-
return {"_raw": raw}
136+
try:
137+
with urllib.request.urlopen(req, timeout=timeout) as resp:
138+
raw = resp.read().decode("utf-8", errors="replace")
139+
except urllib.error.HTTPError as e:
140+
# urlopen raises HTTPError for 4xx/5xx; the response body often
141+
# carries the useful detail (Anthropic "invalid API key" etc.).
142+
err_body = e.read().decode("utf-8", errors="replace")
143+
raise RuntimeError(f"HTTP {e.code}: {err_body[:200]}") from e
144+
try:
145+
return json.loads(raw) if raw else {}
146+
except json.JSONDecodeError:
147+
return {"_raw": raw}
143148

144149

145150
def run_haiku(api_key: str, report: LaneReport) -> None:
@@ -175,14 +180,19 @@ def run_haiku(api_key: str, report: LaneReport) -> None:
175180
if block.get("type") == "text":
176181
text += block.get("text", "")
177182
text = text.strip()
178-
if text.startswith("```"):
179-
text = text.strip("`")
180-
if text.lower().startswith("json"):
181-
text = text[4:].strip()
183+
# Haiku is instructed to return ONLY a JSON object, but extract the
184+
# outermost `{...}` span so we survive the odd case where the model
185+
# adds a prose preamble or wraps the output in a ```json fence.
186+
# Greedy + DOTALL matches first `{` to last `}` — correct for a
187+
# single top-level object, which our schema requires.
188+
match = re.search(r"\{.*\}", text, re.DOTALL)
189+
if match is None:
190+
report.notable = f"haiku returned no JSON object: {text[:160]}"
191+
return
182192
try:
183-
data = json.loads(text)
193+
data = json.loads(match.group(0))
184194
except json.JSONDecodeError:
185-
report.notable = f"haiku returned non-JSON: {text[:160]}"
195+
report.notable = f"haiku JSON parse failed: {match.group(0)[:160]}"
186196
return
187197
if isinstance(data.get("status"), str):
188198
report.status = data["status"]

0 commit comments

Comments
 (0)