From 6d86e8d7f878553077a4b0cb480645e730424822 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Feb 2026 18:51:28 +0100 Subject: [PATCH] fix: retry transient SSL/connection errors in alert backends Add retry loop (3 attempts, exponential backoff) for SSLError, ConnectionError, TimeoutError, and OSError in alert poll cycle. Non-transient errors fail immediately. Also fixes searx test mocks to match direct urlopen usage. Co-Authored-By: Claude Opus 4.6 --- plugins/alert.py | 22 +++++++++++++++++----- tests/test_alert.py | 6 +++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/plugins/alert.py b/plugins/alert.py index 452f8e6..13a612f 100644 --- a/plugins/alert.py +++ b/plugins/alert.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio import json import re +import ssl import urllib.request from datetime import datetime, timezone @@ -271,11 +272,22 @@ async def _poll_once(bot, key: str, announce: bool = True) -> None: loop = asyncio.get_running_loop() for tag, backend in _BACKENDS.items(): - try: - items = await loop.run_in_executor(None, backend, keyword) - except Exception as exc: - data["last_error"] = f"{tag}: {exc}" - had_error = True + items = None + for attempt in range(3): + try: + items = await loop.run_in_executor(None, backend, keyword) + break + except (ssl.SSLError, ConnectionError, TimeoutError, OSError) as exc: + if attempt < 2: + await asyncio.sleep(2 ** attempt) + continue + data["last_error"] = f"{tag}: {exc}" + had_error = True + except Exception as exc: + data["last_error"] = f"{tag}: {exc}" + had_error = True + break + if items is None: continue seen_set = set(data.get("seen", {}).get(tag, [])) diff --git a/tests/test_alert.py b/tests/test_alert.py index c3881d5..ef393f8 100644 --- a/tests/test_alert.py +++ b/tests/test_alert.py @@ -1220,7 +1220,7 @@ class TestSearchSearx: def close(self): pass - with patch.object(_mod, "_urlopen", return_value=FakeResp()): + with patch("urllib.request.urlopen", return_value=FakeResp()): results = _search_searx("test query") assert len(results) == 3 assert results[0]["id"] == "https://example.com/sx1" @@ -1237,13 +1237,13 @@ class TestSearchSearx: def close(self): pass - with patch.object(_mod, "_urlopen", return_value=FakeResp()): + with patch("urllib.request.urlopen", return_value=FakeResp()): results = _search_searx("nothing") assert results == [] def test_http_error_propagates(self): import pytest - with patch.object(_mod, "_urlopen", side_effect=ConnectionError("fail")): + with patch("urllib.request.urlopen", side_effect=ConnectionError("fail")): with pytest.raises(ConnectionError): _search_searx("test")