G7.6: fix the alert path — urllib UA was rejected 403 by Resend
All checks were successful
check / gate (push) Successful in 18s
canary / probe (push) Successful in 22s

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 <noreply@anthropic.com>
This commit is contained in:
Grant Whitmer
2026-08-12 13:44:00 -04:00
parent 9a7030351b
commit fc1937560c
2 changed files with 22 additions and 0 deletions

View File

@@ -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