diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f379ad04..65dc6f340 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: check-merge-conflict - id: check-toml diff --git a/pydis_site/apps/content/tests/test_utils.py b/pydis_site/apps/content/tests/test_utils.py index 09b52205d..769084834 100644 --- a/pydis_site/apps/content/tests/test_utils.py +++ b/pydis_site/apps/content/tests/test_utils.py @@ -241,6 +241,26 @@ def test_get_tag_404(self): with self.assertRaises(models.Tag.DoesNotExist): utils.get_tag("fake") + @mock.patch.object(utils, "fetch_tags") + def test_get_tags_falls_back_to_stale_cache_on_error(self, fetch_mock: mock.Mock): + """Test that stale cached tags are served when refreshing from GitHub fails.""" + stale = models.Tag.objects.create(name="stale-tag") + models.Tag.objects.update(last_updated=_time) + fetch_mock.side_effect = httpx.ReadTimeout("The read operation timed out") + + result = utils.get_tags() + + fetch_mock.assert_called_once() + self.assertEqual([stale], result) + + @mock.patch.object(utils, "fetch_tags") + def test_get_tags_reraises_on_error_with_empty_cache(self, fetch_mock: mock.Mock): + """Test that an error is raised when there is no cached data to fall back on.""" + fetch_mock.side_effect = httpx.ReadTimeout("The read operation timed out") + + with self.assertRaises(utils.TagUpdateError): + utils.get_tags() + @mock.patch.object(utils, "get_tag_category") def test_category_pages(self, get_mock: mock.Mock): """Test that the category pages function calls the correct method for tags.""" diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py index bb892d350..2d8f90aed 100644 --- a/pydis_site/apps/content/utils.py +++ b/pydis_site/apps/content/utils.py @@ -23,6 +23,10 @@ log = logging.getLogger(__name__) +class TagUpdateError(Exception): + """Raised when tags cannot be fetched from GitHub and no cached data is available.""" + + def github_client(**kwargs) -> httpx.Client: """Get a client to access the GitHub API with important settings pre-configured.""" client = httpx.Client( @@ -228,7 +232,16 @@ def get_tags() -> list[Tag]: if settings.STATIC_BUILD: # pragma: no cover tags = get_tags_static() else: - tags = fetch_tags() + try: + tags = fetch_tags() + except httpx.HTTPError as error: + # return the stale cache if we have one, otherwise raise the error + if last_update is not None: + log.warning("Failed to refresh tags from GitHub: %r", error) + return list(Tag.objects.all()) + raise TagUpdateError( + "Failed to fetch tags from GitHub, also have no stale cache to fall back on." + ) from error record_tags(tags) return tags diff --git a/pydis_site/apps/home/views.py b/pydis_site/apps/home/views.py index 41415f846..859d5e30d 100644 --- a/pydis_site/apps/home/views.py +++ b/pydis_site/apps/home/views.py @@ -79,10 +79,10 @@ def _get_api_data(self) -> dict[str, RepoAPIData]: api_data: list[dict] = resp.json() except httpx.TimeoutException: - log.error("Request to fetch GitHub repository metadata for timed out!") + log.warning("Request to fetch GitHub repository metadata for timed out!") return repo_dict except httpx.HTTPStatusError as ex: - log.error(f"Received HTTP {ex.response.status_code} from GitHub repository metadata request!") + log.warning(f"Received HTTP {ex.response.status_code} from GitHub repository metadata request!") return repo_dict except JSONDecodeError: log.error("GitHub returned invalid JSON for repository metadata!")