diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ flask==3.0.3 -requests==2.31.0 +requests==2.32.3 redis==5.0.4 diff --git a/webhooks/deliver.py b/webhooks/deliver.py --- a/webhooks/deliver.py +++ b/webhooks/deliver.py @@ -1,14 +1,27 @@ import logging +import time import requests log = logging.getLogger(__name__) +MAX_ATTEMPTS = 4 +BACKOFF_SECONDS = (1, 4, 16) def deliver(url, payload): - resp = requests.post(url, json=payload, timeout=5) - if resp.status_code >= 400: - log.warning("delivery failed: %s", resp.status_code) - return False - return True + for attempt in range(1, MAX_ATTEMPTS + 1): + resp = requests.post(url, json=payload, timeout=5) + if resp.status_code < 400: + return True + if resp.status_code < 500: + log.warning("delivery rejected: %s", resp.status_code) + return False + log.warning("delivery failed (attempt %d): %s", attempt, resp.status_code) + if attempt < MAX_ATTEMPTS: + time.sleep(BACKOFF_SECONDS[attempt - 1]) + return False diff --git a/tests/test_deliver.py b/tests/test_deliver.py --- a/tests/test_deliver.py +++ b/tests/test_deliver.py @@ -10,12 +10,22 @@ def test_delivery_ok(mock_post): assert deliver("https://example.test/hook", {"id": 1}) is True -def test_delivery_timeout(mock_post): - mock_post.side_effect = requests.Timeout - with pytest.raises(requests.Timeout): - deliver("https://example.test/hook", {"id": 1}) +def test_retry_on_503(mock_post, monkeypatch): + monkeypatch.setattr("webhooks.deliver.time.sleep", lambda s: None) + mock_post.side_effect = [Resp(503), Resp(503), Resp(200)] + assert deliver("https://example.test/hook", {"id": 1}) is True + assert mock_post.call_count == 3 + + +def test_no_retry_on_404(mock_post): + mock_post.return_value = Resp(404) + assert deliver("https://example.test/hook", {"id": 1}) is False + assert mock_post.call_count == 1