diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index fb466886c..3fafaa68b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1560,6 +1560,7 @@ def _do_rest_request( response = None new_response = None for retry_idx in reversed(range(max_retries)): + new_response = None try: response = function(url, **kwargs) diff --git a/tests/fake_transfer.py b/tests/fake_transfer.py new file mode 100644 index 000000000..536c62307 --- /dev/null +++ b/tests/fake_transfer.py @@ -0,0 +1,41 @@ +"""Helpers to test requests and file transfers without AYON server.""" +import requests +from requests.structures import CaseInsensitiveDict + + +class FakeResponse: + """Minimal stand-in for 'requests.Response'.""" + def __init__( + self, status_code=200, content=b"", headers=None, json_data=None + ): + self.status_code = status_code + self.content = content + self.headers = CaseInsensitiveDict(headers or {}) + self._json_data = json_data + self.reason = "Reason" + self.text = content.decode(errors="ignore") + + @property + def ok(self): + return self.status_code < 400 + + def raise_for_status(self): + if not self.ok: + raise requests.exceptions.HTTPError( + f"{self.status_code} Error", response=self + ) + + def json(self): + if self._json_data is None: + raise ValueError("No json") + return self._json_data + + def iter_content(self, chunk_size=1): + for idx in range(0, len(self.content), chunk_size): + yield self.content[idx:idx + chunk_size] + + def __enter__(self): + return self + + def __exit__(self, *args): + return False diff --git a/tests/test_request_retries.py b/tests/test_request_retries.py new file mode 100644 index 000000000..22708c78c --- /dev/null +++ b/tests/test_request_retries.py @@ -0,0 +1,35 @@ +"""Retries of REST requests. Does not require running AYON server.""" +import pytest +import requests + +from ayon_api.server_api import ServerAPI + +from .fake_transfer import FakeResponse + + +@pytest.fixture +def con(monkeypatch): + monkeypatch.setattr("time.sleep", lambda *args, **kwargs: None) + return ServerAPI("http://localhost:0", create_session=False, max_retries=3) + + +@pytest.mark.parametrize( + "failure", + [requests.exceptions.ConnectionError("down"), FakeResponse(503)], +) +def test_successful_retry_returns_successful_response(con, failure): + responses = [failure, FakeResponse(200, json_data={"ok": True})] + + # `_do_rest_request` expects a callable that takes a URL and returns a + # response. So we just return fake responses to simulate it. + def request_func(url, **kwargs): + item = responses.pop(0) + if isinstance(item, Exception): + raise item + return item + + response = con._do_rest_request( + request_func, "http://localhost:0/api/x", handle_invalid_token=False + ) + assert response.status_code == 200 + assert response.data == {"ok": True}