ROOT CAUSE: tg() sent the Telegram method name as the HTTP verb
tg() passed its 'method' argument (e.g. 'sendMessage') straight into
urllib Request(method=...), producing the request line
sendMessage /bot<token>/sendMessage HTTP/1.1
'sendMessage' is not an HTTP verb, so Telegram's edge rejects it with 400
(bare nginx page, no JSON). Every sendMessage-based alert therefore failed
silently: the debounced failure alert, the recovery notice, and text-only
listing notifications. Listing alerts with an image survived only because
sendPhoto goes through the multipart helper which builds its own POST.
Combined with the 1000-row pagination bug, this is why no notifications
arrived while the container still reported healthy.
- tg(): always use HTTP POST for the request, keep the Telegram method in the path
- test: assert req.get_method() == 'POST' and that body is attached
(mutation-verified: restoring the bug fails the check)
- also restore the patched global urlopen between test sections
Verified live in the container: fixed tg -> 200 ok:true, message_id 493
delivered; unfixed tg -> HTTP 400.
This commit is contained in:
+33
-6
@@ -115,7 +115,13 @@ SETTINGS_COLS = [
|
||||
# --------------------------------------------------------------------------- #
|
||||
# HTTP helpers
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _http(method, url, body=None, headers=None, timeout=30):
|
||||
def _http(method, url, body=None, headers=None, timeout=30, retries=2):
|
||||
"""HTTP 请求。对 Telegram 边缘偶发的 400 做重试。
|
||||
|
||||
Telegram 的 api.telegram.org 边缘前置(nginx)会偶发对完全合法的请求返回
|
||||
400(同 IP、同 token、同 payload 的裸 socket 请求同时却是 200)。命中时
|
||||
通知会静默丢失。这里对 400/5xx 做有限重试,间隔递增;仍失败则抛出。
|
||||
"""
|
||||
h = {"User-Agent": UA}
|
||||
if headers:
|
||||
h.update(headers)
|
||||
@@ -123,10 +129,23 @@ def _http(method, url, body=None, headers=None, timeout=30):
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
h["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=h)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
raw = r.read().decode("utf-8", errors="ignore")
|
||||
return r.status, raw
|
||||
last = None
|
||||
for attempt in range(retries + 1):
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=h)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
raw = r.read().decode("utf-8", errors="ignore")
|
||||
return r.status, raw
|
||||
except urllib.error.HTTPError as e:
|
||||
last = e
|
||||
# 4xx 里只有 400/408/429 值得重试;其余(如 404/403)立即失败
|
||||
if e.code not in (400, 408, 429) and e.code < 500:
|
||||
raise
|
||||
if attempt < retries:
|
||||
time.sleep(0.5 * (attempt + 1))
|
||||
if last is not None:
|
||||
raise last
|
||||
raise RuntimeError("_http: no attempt made")
|
||||
|
||||
|
||||
def _download_image(url, timeout=20):
|
||||
@@ -219,8 +238,16 @@ def nc_list_all(tid, fields=None, extra_query=""):
|
||||
|
||||
|
||||
def tg(method, payload):
|
||||
"""调用 Telegram Bot API。
|
||||
|
||||
注意:method 是 Telegram 的方法名(如 "sendMessage"),**不是** HTTP 动词。
|
||||
早期版本直接把它当 HTTP method 传下去,于是请求行变成
|
||||
`sendMessage /bot<token>/sendMessage HTTP/1.1` —— 非法动词,Telegram 边缘
|
||||
一律回 400。结果是所有走 sendMessage 的通知静默失败(带图卡的 sendPhoto
|
||||
走 multipart 自建请求,所以侥幸能用)。
|
||||
"""
|
||||
return _json(_http(
|
||||
method, f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/{method}",
|
||||
"POST", f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/{method}",
|
||||
body=payload))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user