From fc1937560c6d0083105a9b91ad7e456aa74d1668 Mon Sep 17 00:00:00 2001 From: Grant Whitmer Date: Wed, 12 Aug 2026 13:44:00 -0400 Subject: [PATCH] =?UTF-8?q?G7.6:=20fix=20the=20alert=20path=20=E2=80=94=20?= =?UTF-8?q?urllib=20UA=20was=20rejected=20403=20by=20Resend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught by TESTING the alert path instead of assuming it. Without an explicit User-Agent, urllib sends 'Python-urllib/3.x' and Resend rejects it 403, while the identical request via curl succeeds. The failure mode this avoids is the worst one a canary has: it would have detected every outage correctly and told nobody. Same bot-filtering trap as the Gitea migrate call earlier today — worth recognising on sight. Also prints the HTTP body on failure. '403 Forbidden' alone sends you hunting for a bad key; the body names the real cause. Verified: alert sent (200). Co-Authored-By: Claude Opus 5 --- api/tests/test_invariants.py | 10 ++++++++++ scripts/canary.py | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/api/tests/test_invariants.py b/api/tests/test_invariants.py index 2328f49..734cdfc 100644 --- a/api/tests/test_invariants.py +++ b/api/tests/test_invariants.py @@ -537,3 +537,13 @@ def test_g76_canary_has_two_independent_signals(): had one signal and nothing watched the watcher.""" src = (ROOT / "scripts" / "canary.py").read_text() assert "return 1 if any" in src, "canary must exit non-zero so CI goes red" + + +def test_g76_alert_path_sets_a_user_agent(): + """Without an explicit User-Agent, urllib sends 'Python-urllib/3.x' and + Resend rejects it 403 while the identical curl succeeds. Caught by testing + the alert path: the canary would have detected every outage correctly and + told nobody.""" + src = (ROOT / "scripts" / "canary.py").read_text() + send = src[src.index("def send_alert") : src.index("def main(")] + assert "User-Agent" in send diff --git a/scripts/canary.py b/scripts/canary.py index 47016f7..823c022 100755 --- a/scripts/canary.py +++ b/scripts/canary.py @@ -177,12 +177,24 @@ def send_alert(subject: str, lines: list[str]) -> bool: headers={ "Authorization": f"Bearer {RESEND_KEY}", "Content-Type": "application/json", + # ⚠️ REQUIRED. Without an explicit User-Agent, urllib sends + # "Python-urllib/3.x" and the request is rejected 403 by bot + # filtering — while the identical request via curl succeeds. This + # exact failure was caught by testing the alert path rather than + # assuming it: the canary would have detected every outage + # correctly and told nobody. + "User-Agent": "windy-git-canary/1.0", }, ) try: with urllib.request.urlopen(req, timeout=30) as r: print(f" alert sent ({r.status})") return True + except urllib.error.HTTPError as e: + # Print the body. "403 Forbidden" alone sends you hunting for a bad key; + # the body usually names the real cause. + print(f"!! alert FAILED: HTTP {e.code}: {e.read().decode()[:200]}") + return False except Exception as e: # noqa: BLE001 print(f"!! alert FAILED: {e}") return False