From 17a2d44abc4d722d9052fa824faa73f470d777da Mon Sep 17 00:00:00 2001 From: Samiul Sk Date: Thu, 6 Nov 2025 23:15:38 +0530 Subject: [PATCH 1/2] Add validation for decorator order with @api_view Raise TypeError when API policy decorators (@permission_classes, @renderer_classes, etc.) are applied after @api_view instead of before it. Fixes #9596 --- rest_framework/decorators.py | 26 ++++++++ tests/test_decorators.py | 118 +++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 93e0751b7a..da97f0388b 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -87,8 +87,26 @@ def handler(self, *args, **kwargs): return decorator +def _check_decorator_order(func, decorator_name): + """ + Check if an API policy decorator is being applied after @api_view. + """ + # Check if func is actually a view function (result of APIView.as_view()) + if hasattr(func, 'cls') and issubclass(func.cls, APIView): + raise TypeError( + f"@{decorator_name} must be applied before @api_view. " + f"The correct order is:\n\n" + f" @api_view(['GET'])\n" + f" @{decorator_name}(...)\n" + f" def my_view(request):\n" + f" ...\n\n" + f"See https://www.django-rest-framework.org/api-guide/views/#api-policy-decorators" + ) + + def renderer_classes(renderer_classes): def decorator(func): + _check_decorator_order(func, 'renderer_classes') func.renderer_classes = renderer_classes return func return decorator @@ -96,6 +114,7 @@ def decorator(func): def parser_classes(parser_classes): def decorator(func): + _check_decorator_order(func, 'parser_classes') func.parser_classes = parser_classes return func return decorator @@ -103,6 +122,7 @@ def decorator(func): def authentication_classes(authentication_classes): def decorator(func): + _check_decorator_order(func, 'authentication_classes') func.authentication_classes = authentication_classes return func return decorator @@ -110,6 +130,7 @@ def decorator(func): def throttle_classes(throttle_classes): def decorator(func): + _check_decorator_order(func, 'throttle_classes') func.throttle_classes = throttle_classes return func return decorator @@ -117,6 +138,7 @@ def decorator(func): def permission_classes(permission_classes): def decorator(func): + _check_decorator_order(func, 'permission_classes') func.permission_classes = permission_classes return func return decorator @@ -124,6 +146,7 @@ def decorator(func): def content_negotiation_class(content_negotiation_class): def decorator(func): + _check_decorator_order(func, 'content_negotiation_class') func.content_negotiation_class = content_negotiation_class return func return decorator @@ -131,6 +154,7 @@ def decorator(func): def metadata_class(metadata_class): def decorator(func): + _check_decorator_order(func, 'metadata_class') func.metadata_class = metadata_class return func return decorator @@ -138,6 +162,7 @@ def decorator(func): def versioning_class(versioning_class): def decorator(func): + _check_decorator_order(func, 'versioning_class') func.versioning_class = versioning_class return func return decorator @@ -145,6 +170,7 @@ def decorator(func): def schema(view_inspector): def decorator(func): + _check_decorator_order(func, 'schema') func.schema = view_inspector return func return decorator diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 0c070bc10b..81d34fac4b 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -204,6 +204,124 @@ def view(request): assert isinstance(view.cls.schema, CustomSchema) + def test_incorrect_decorator_order_permission_classes(self): + """ + If @permission_classes is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @permission_classes([IsAuthenticated]) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@permission_classes must be applied before @api_view' in str(cm.exception) + + def test_incorrect_decorator_order_renderer_classes(self): + """ + If @renderer_classes is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @renderer_classes([JSONRenderer]) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@renderer_classes must be applied before @api_view' in str(cm.exception) + + def test_incorrect_decorator_order_parser_classes(self): + """ + If @parser_classes is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @parser_classes([JSONParser]) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@parser_classes must be applied before @api_view' in str(cm.exception) + + def test_incorrect_decorator_order_authentication_classes(self): + """ + If @authentication_classes is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @authentication_classes([BasicAuthentication]) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@authentication_classes must be applied before @api_view' in str(cm.exception) + + def test_incorrect_decorator_order_throttle_classes(self): + """ + If @throttle_classes is applied after @api_view, we should raise a TypeError. + """ + class OncePerDayUserThrottle(UserRateThrottle): + rate = '1/day' + + with self.assertRaises(TypeError) as cm: + @throttle_classes([OncePerDayUserThrottle]) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@throttle_classes must be applied before @api_view' in str(cm.exception) + + def test_incorrect_decorator_order_versioning_class(self): + """ + If @versioning_class is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @versioning_class(QueryParameterVersioning) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@versioning_class must be applied before @api_view' in str(cm.exception) + + def test_incorrect_decorator_order_metadata_class(self): + """ + If @metadata_class is applied after @api_view, we should raise a TypeError. + """ + with self.assertRaises(TypeError) as cm: + @metadata_class(None) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@metadata_class must be applied before @api_view' in str(cm.exception) + + def test_incorrect_decorator_order_content_negotiation_class(self): + """ + If @content_negotiation_class is applied after @api_view, we should raise a TypeError. + """ + class CustomContentNegotiation(BaseContentNegotiation): + def select_renderer(self, request, renderers, format_suffix): + return (renderers[0], renderers[0].media_type) + + with self.assertRaises(TypeError) as cm: + @content_negotiation_class(CustomContentNegotiation) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@content_negotiation_class must be applied before @api_view' in str(cm.exception) + + def test_incorrect_decorator_order_schema(self): + """ + If @schema is applied after @api_view, we should raise a TypeError. + """ + class CustomSchema(AutoSchema): + pass + + with self.assertRaises(TypeError) as cm: + @schema(CustomSchema()) + @api_view(['GET']) + def view(request): + return Response({}) + + assert '@schema must be applied before @api_view' in str(cm.exception) + class ActionDecoratorTestCase(TestCase): From b8685f28b2fb2a6bc0a89218f43cd2e9beb79fb3 Mon Sep 17 00:00:00 2001 From: Samiul Sk Date: Fri, 7 Nov 2025 11:58:20 +0530 Subject: [PATCH 2/2] Address PR review feedback: update error message wording and example - Change 'must be applied before' to 'must come after (below) the' to match DRF docs language - Fix decorator order in example to show @api_view first, then policy decorator below - Remove unnecessary f-string prefixes from non-interpolated lines - Update all test assertions to match new error message wording Addresses feedback from @browniebroke in PR #9821 --- rest_framework/decorators.py | 12 ++++++------ tests/test_decorators.py | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index da97f0388b..a69a613ba4 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -94,13 +94,13 @@ def _check_decorator_order(func, decorator_name): # Check if func is actually a view function (result of APIView.as_view()) if hasattr(func, 'cls') and issubclass(func.cls, APIView): raise TypeError( - f"@{decorator_name} must be applied before @api_view. " - f"The correct order is:\n\n" - f" @api_view(['GET'])\n" + f"@{decorator_name} must come after (below) the @api_view decorator. " + "The correct order is:\n\n" + " @api_view(['GET'])\n" f" @{decorator_name}(...)\n" - f" def my_view(request):\n" - f" ...\n\n" - f"See https://www.django-rest-framework.org/api-guide/views/#api-policy-decorators" + " def my_view(request):\n" + " ...\n\n" + "See https://www.django-rest-framework.org/api-guide/views/#api-policy-decorators" ) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 81d34fac4b..cc7cab4d7d 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -214,7 +214,7 @@ def test_incorrect_decorator_order_permission_classes(self): def view(request): return Response({}) - assert '@permission_classes must be applied before @api_view' in str(cm.exception) + assert '@permission_classes must come after (below) the @api_view decorator' in str(cm.exception) def test_incorrect_decorator_order_renderer_classes(self): """ @@ -226,7 +226,7 @@ def test_incorrect_decorator_order_renderer_classes(self): def view(request): return Response({}) - assert '@renderer_classes must be applied before @api_view' in str(cm.exception) + assert '@renderer_classes must come after (below) the @api_view decorator' in str(cm.exception) def test_incorrect_decorator_order_parser_classes(self): """ @@ -238,7 +238,7 @@ def test_incorrect_decorator_order_parser_classes(self): def view(request): return Response({}) - assert '@parser_classes must be applied before @api_view' in str(cm.exception) + assert '@parser_classes must come after (below) the @api_view decorator' in str(cm.exception) def test_incorrect_decorator_order_authentication_classes(self): """ @@ -250,7 +250,7 @@ def test_incorrect_decorator_order_authentication_classes(self): def view(request): return Response({}) - assert '@authentication_classes must be applied before @api_view' in str(cm.exception) + assert '@authentication_classes must come after (below) the @api_view decorator' in str(cm.exception) def test_incorrect_decorator_order_throttle_classes(self): """ @@ -265,7 +265,7 @@ class OncePerDayUserThrottle(UserRateThrottle): def view(request): return Response({}) - assert '@throttle_classes must be applied before @api_view' in str(cm.exception) + assert '@throttle_classes must come after (below) the @api_view decorator' in str(cm.exception) def test_incorrect_decorator_order_versioning_class(self): """ @@ -277,7 +277,7 @@ def test_incorrect_decorator_order_versioning_class(self): def view(request): return Response({}) - assert '@versioning_class must be applied before @api_view' in str(cm.exception) + assert '@versioning_class must come after (below) the @api_view decorator' in str(cm.exception) def test_incorrect_decorator_order_metadata_class(self): """ @@ -289,7 +289,7 @@ def test_incorrect_decorator_order_metadata_class(self): def view(request): return Response({}) - assert '@metadata_class must be applied before @api_view' in str(cm.exception) + assert '@metadata_class must come after (below) the @api_view decorator' in str(cm.exception) def test_incorrect_decorator_order_content_negotiation_class(self): """ @@ -305,7 +305,7 @@ def select_renderer(self, request, renderers, format_suffix): def view(request): return Response({}) - assert '@content_negotiation_class must be applied before @api_view' in str(cm.exception) + assert '@content_negotiation_class must come after (below) the @api_view decorator' in str(cm.exception) def test_incorrect_decorator_order_schema(self): """ @@ -320,7 +320,7 @@ class CustomSchema(AutoSchema): def view(request): return Response({}) - assert '@schema must be applied before @api_view' in str(cm.exception) + assert '@schema must come after (below) the @api_view decorator' in str(cm.exception) class ActionDecoratorTestCase(TestCase):