Skip to content

Commit e6a74f4

Browse files
authored
Merge pull request #1213 from thunderstore-io/cache-by-mixin
Replaced Cache-Control header definitions with generic Mixin
2 parents d81d0aa + 1a9b1dd commit e6a74f4

File tree

10 files changed

+62
-18
lines changed

10 files changed

+62
-18
lines changed
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from rest_framework.generics import RetrieveAPIView
22

33
from thunderstore.api.cyberstorm.serializers import CyberstormCommunitySerializer
4-
from thunderstore.api.utils import CyberstormAutoSchemaMixin
4+
from thunderstore.api.utils import CyberstormAutoSchemaMixin, PublicCacheMixin
55
from thunderstore.community.models import Community
66

77

8-
class CommunityAPIView(CyberstormAutoSchemaMixin, RetrieveAPIView):
8+
class CommunityAPIView(PublicCacheMixin, CyberstormAutoSchemaMixin, RetrieveAPIView):
99
lookup_url_kwarg = "community_id"
1010
lookup_field = "identifier"
1111
permission_classes = []
@@ -16,5 +16,4 @@ class CommunityAPIView(CyberstormAutoSchemaMixin, RetrieveAPIView):
1616

1717
def get(self, *args, **kwargs):
1818
response = super().get(*args, **kwargs)
19-
response["Cache-Control"] = "public, max-age=60"
2019
return response

django/thunderstore/api/cyberstorm/views/community_filters.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
CyberstormPackageCategorySerializer,
99
CyberstormPackageListingSectionSerializer,
1010
)
11-
from thunderstore.api.utils import conditional_swagger_auto_schema
11+
from thunderstore.api.utils import PublicCacheMixin, conditional_swagger_auto_schema
1212
from thunderstore.community.models import Community
1313

1414

@@ -17,7 +17,7 @@ class CommunityFiltersAPIViewSerializer(serializers.Serializer):
1717
sections = CyberstormPackageListingSectionSerializer(many=True)
1818

1919

20-
class CommunityFiltersAPIView(APIView):
20+
class CommunityFiltersAPIView(PublicCacheMixin, APIView):
2121
"""
2222
Return info about PackageCategories and PackageListingSections so
2323
they can be used as filters.
@@ -40,5 +40,4 @@ def get(self, request: Request, community_id: str):
4040
}
4141

4242
response = Response(self.serializer_class(filters).data)
43-
response["Cache-Control"] = "public, max-age=60"
4443
return response

django/thunderstore/api/cyberstorm/views/community_list.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from thunderstore.api.ordering import StrictOrderingFilter
88
from thunderstore.api.utils import (
99
CyberstormAutoSchemaMixin,
10+
PublicCacheMixin,
1011
conditional_swagger_auto_schema,
1112
)
1213
from thunderstore.community.models import Community
@@ -20,7 +21,7 @@ class CommunityListAPIQueryParams(serializers.Serializer):
2021
include_unlisted = serializers.BooleanField(default=False)
2122

2223

23-
class CommunityListAPIView(CyberstormAutoSchemaMixin, ListAPIView):
24+
class CommunityListAPIView(PublicCacheMixin, CyberstormAutoSchemaMixin, ListAPIView):
2425
permission_classes = []
2526
serializer_class = CyberstormCommunitySerializer
2627
pagination_class = CommunityPaginator
@@ -50,5 +51,4 @@ def get_queryset(self):
5051
)
5152
def get(self, *args, **kwargs):
5253
response = super().get(*args, **kwargs)
53-
response["Cache-Control"] = "public, max-age=60"
5454
return response

django/thunderstore/api/cyberstorm/views/markdown.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from rest_framework import serializers
55
from rest_framework.generics import RetrieveAPIView, get_object_or_404
66

7-
from thunderstore.api.utils import CyberstormAutoSchemaMixin
7+
from thunderstore.api.utils import CyberstormAutoSchemaMixin, PublicCacheMixin
88
from thunderstore.markdown.templatetags.markdownify import render_markdown
99
from thunderstore.repository.models import Package, PackageVersion
1010

@@ -13,7 +13,9 @@ class CyberstormMarkdownResponseSerializer(serializers.Serializer):
1313
html = serializers.CharField()
1414

1515

16-
class PackageVersionReadmeAPIView(CyberstormAutoSchemaMixin, RetrieveAPIView):
16+
class PackageVersionReadmeAPIView(
17+
PublicCacheMixin, CyberstormAutoSchemaMixin, RetrieveAPIView
18+
):
1719
"""
1820
Return README.md prerendered as HTML.
1921
@@ -32,7 +34,9 @@ def get_object(self):
3234
return {"html": render_markdown(package_version.readme)}
3335

3436

35-
class PackageVersionChangelogAPIView(CyberstormAutoSchemaMixin, RetrieveAPIView):
37+
class PackageVersionChangelogAPIView(
38+
PublicCacheMixin, CyberstormAutoSchemaMixin, RetrieveAPIView
39+
):
3640
"""
3741
Return CHANGELOG.md prerendered as HTML.
3842
@@ -41,6 +45,8 @@ class PackageVersionChangelogAPIView(CyberstormAutoSchemaMixin, RetrieveAPIView)
4145

4246
serializer_class = CyberstormMarkdownResponseSerializer
4347

48+
cache_404s = True
49+
4450
def get_object(self):
4551
package_version = get_package_version(
4652
namespace_id=self.kwargs["namespace_id"],

django/thunderstore/api/cyberstorm/views/package_listing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
)
3333
from thunderstore.api.utils import (
3434
CyberstormAutoSchemaMixin,
35+
PublicCacheMixin,
3536
conditional_swagger_auto_schema,
3637
)
3738
from thunderstore.community.models.package_listing import PackageListing

django/thunderstore/api/cyberstorm/views/package_listing_list.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from rest_framework.pagination import PageNumberPagination
1313

1414
from thunderstore.api.cyberstorm.serializers import CyberstormPackagePreviewSerializer
15-
from thunderstore.api.utils import conditional_swagger_auto_schema
15+
from thunderstore.api.utils import PublicCacheMixin, conditional_swagger_auto_schema
1616
from thunderstore.community.consts import PackageListingReviewStatus
1717
from thunderstore.community.models import (
1818
Community,
@@ -81,7 +81,7 @@ def get_schema_fields(self, view):
8181
return []
8282

8383

84-
class BasePackageListAPIView(ListAPIView):
84+
class BasePackageListAPIView(PublicCacheMixin, ListAPIView):
8585
"""
8686
Base class for community-scoped, paginated, filterable package listings.
8787

django/thunderstore/api/cyberstorm/views/package_version.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
)
1111
from thunderstore.api.utils import (
1212
CyberstormAutoSchemaMixin,
13+
PublicCacheMixin,
1314
conditional_swagger_auto_schema,
1415
)
1516
from thunderstore.repository.models.package_version import PackageVersion
1617

1718

18-
class PackageVersionAPIView(CyberstormAutoSchemaMixin, APIView):
19+
class PackageVersionAPIView(PublicCacheMixin, CyberstormAutoSchemaMixin, APIView):
1920
@conditional_swagger_auto_schema(
2021
responses={200: PackageVersionResponseSerializer},
2122
operation_id="cyberstorm.package_version",

django/thunderstore/api/cyberstorm/views/package_version_list.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
)
88
from thunderstore.api.cyberstorm.views.markdown import get_package_version
99
from thunderstore.api.pagination import PackageDependenciesPaginator
10-
from thunderstore.api.utils import CyberstormAutoSchemaMixin
10+
from thunderstore.api.utils import CyberstormAutoSchemaMixin, PublicCacheMixin
1111
from thunderstore.repository.models import Package, PackageVersion
1212

1313

@@ -19,7 +19,9 @@ class CyberstormPackageVersionSerializer(serializers.Serializer):
1919
install_url = serializers.CharField()
2020

2121

22-
class PackageVersionListAPIView(CyberstormAutoSchemaMixin, ListAPIView):
22+
class PackageVersionListAPIView(
23+
PublicCacheMixin, CyberstormAutoSchemaMixin, ListAPIView
24+
):
2325
"""
2426
Return a list of available versions of the package.
2527
"""
@@ -36,7 +38,9 @@ def get_queryset(self):
3638
return package.versions.active()
3739

3840

39-
class PackageVersionDependenciesListAPIView(CyberstormAutoSchemaMixin, ListAPIView):
41+
class PackageVersionDependenciesListAPIView(
42+
PublicCacheMixin, CyberstormAutoSchemaMixin, ListAPIView
43+
):
4044
serializer_class = CyberstormPackageDependencySerializer
4145
pagination_class = PackageDependenciesPaginator
4246

django/thunderstore/api/cyberstorm/views/team.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from thunderstore.api.ordering import StrictOrderingFilter
3232
from thunderstore.api.utils import (
3333
CyberstormAutoSchemaMixin,
34+
PublicCacheMixin,
3435
conditional_swagger_auto_schema,
3536
)
3637
from thunderstore.repository.forms import AddTeamMemberForm
@@ -52,7 +53,7 @@ def check_permissions(self, request: Request) -> None:
5253
raise PermissionDenied("You do not have permission to access this team.")
5354

5455

55-
class TeamAPIView(CyberstormAutoSchemaMixin, RetrieveAPIView):
56+
class TeamAPIView(PublicCacheMixin, CyberstormAutoSchemaMixin, RetrieveAPIView):
5657
serializer_class = CyberstormTeamSerializer
5758
queryset = Team.objects.exclude(is_active=False)
5859
lookup_field = "name"

django/thunderstore/api/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.conf import settings
2+
from django.utils.cache import patch_cache_control
23
from drf_yasg.utils import swagger_auto_schema, unset # type: ignore
34

45

@@ -22,3 +23,35 @@ class CyberstormAutoSchemaMixin: # pragma: no cover
2223
@conditional_swagger_auto_schema(tags=["cyberstorm"])
2324
def get(self, *args, **kwargs):
2425
return super().get(*args, **kwargs)
26+
27+
28+
class PublicCacheMixin:
29+
"""
30+
A mixin for caching public API endpoints.
31+
32+
IMPORTANT: Must be before generic DRF view base classes in the inheritance list.
33+
34+
Example:
35+
class ProductListView(PublicCacheMixin, ListAPIView):
36+
37+
1. Caching: Applies 'public' Cache-Control headers to the response.
38+
2. Security: Explicitly clears 'authentication_classes' and 'permission_classes'
39+
to override global DRF settings in settings.py. This ensures the endpoint is strictly
40+
anonymous and prevents 'request.user' from being populated, which
41+
mitigates the risk of caching user-specific data.
42+
"""
43+
44+
authentication_classes = []
45+
permission_classes = []
46+
47+
cache_max_age = 60 # seconds
48+
cache_404s = False
49+
50+
def finalize_response(self, request, response, *args, **kwargs):
51+
response = super().finalize_response(request, response, *args, **kwargs)
52+
53+
if response.status_code == 200 or (
54+
response.status_code == 404 and self.cache_404s
55+
):
56+
patch_cache_control(response, public=True, max_age=self.cache_max_age)
57+
return response

0 commit comments

Comments
 (0)