From ad180eb7fb03ac55b19babd1b58f20bfec1ad626 Mon Sep 17 00:00:00 2001 From: Yajat Sharma Date: Tue, 29 Sep 2026 01:09:34 +0530 Subject: [PATCH] Fix Resend requests being blocked by Cloudflare (missing User-Agent) _post_to_resend() sent no User-Agent header, so it fell through to Python's default "Python-urllib/x.y" signature -- Resend's Cloudflare WAF blocks that outright (HTTP 403, Cloudflare error 1010) before the request ever reaches Resend's own API. Found during a controlled real -send verification against Resend's sandbox sender (onboarding@resend.dev -> yajats@gmail.com, using a disposable local SQLite database, never production Neon): the request was rejected by Cloudflare, not Resend, until a normal User-Agent was added, after which Resend accepted it (200) and the local delivery row correctly recorded SENT. Also captures the HTTPError response body (previously only status/reason) so a future failure like the "domain not verified" 403 encountered during this same verification is diagnosable from the stored error message alone, without needing to reproduce it manually. send_weekly_summary() in app.py and _send_via_resend() in services/pin_recovery_service.py have the same missing-User-Agent gap and are very likely affected the same way in production -- deliberately left untouched here, out of scope for this fix. Co-Authored-By: Claude Sonnet 5 --- services/monthly_summary_delivery.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/services/monthly_summary_delivery.py b/services/monthly_summary_delivery.py index 7a13585..9291318 100644 --- a/services/monthly_summary_delivery.py +++ b/services/monthly_summary_delivery.py @@ -120,7 +120,17 @@ def _post_to_resend(api_key, from_email, to_email, subject, html_body): services/pin_recovery_service.py's _send_via_resend(). Kept local to this module rather than extracted into a shared helper — the existing call sites are out of scope for this feature (see architectural notes - in the project's Phase 0 audit; not refactoring them here).""" + in the project's Phase 0 audit; not refactoring them here). + + Sets an explicit User-Agent: without one, urllib's default + "Python-urllib/x.y" signature is blocked outright by Resend's + Cloudflare WAF (HTTP 403, Cloudflare error 1010) before the request + ever reaches Resend's own API — discovered during a controlled real + -send verification. The two pre-existing Resend call sites this + module mirrors (send_weekly_summary() in app.py, + services/pin_recovery_service.py's _send_via_resend()) have the same + gap and were very likely failing silently in production for the same + reason; fixing those is explicitly out of scope for this change.""" import json as _json import urllib.request as _url_req import urllib.error as _url_err @@ -135,7 +145,11 @@ def _post_to_resend(api_key, from_email, to_email, subject, html_body): req = _url_req.Request( "https://api.resend.com/emails", data=payload, - headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "User-Agent": "RunRush/1.0 (+https://runrush.onrender.com)", + }, ) try: with _url_req.urlopen(req, timeout=10) as resp: @@ -143,7 +157,11 @@ def _post_to_resend(api_key, from_email, to_email, subject, html_body): return True, None return False, f"Resend returned status {resp.status}" except _url_err.HTTPError as e: - return False, f"HTTPError {e.code}: {e.reason}" + try: + body = e.read().decode("utf-8", errors="replace") + except Exception: + body = "" + return False, f"HTTPError {e.code}: {e.reason}" + (f" - {body}" if body else "") except _url_err.URLError as e: return False, f"URLError: {e.reason}" except Exception as e: # defensive: a send failure must never crash the batch