From b7a17b0ea0a2061bae752a3a2292007d41825814 Mon Sep 17 00:00:00 2001
From: Ben Lomax <lomax.on.the.run@gmail.com>
Date: Sat, 8 Jul 2023 21:54:37 +0100
Subject: [PATCH] Refs #31949 -- Made @vary_on_(cookie/headers) decorators work
 with async functions.

---
 django/views/decorators/vary.py | 22 ++++++++---
 docs/releases/5.0.txt           |  2 +
 docs/topics/async.txt           |  2 +
 docs/topics/http/decorators.txt |  8 ++++
 tests/decorators/test_vary.py   | 69 +++++++++++++++++++++++++++++++++
 5 files changed, 97 insertions(+), 6 deletions(-)
 create mode 100644 tests/decorators/test_vary.py

diff --git a/django/views/decorators/vary.py b/django/views/decorators/vary.py
index b1459295d2..9beab8b4db 100644
--- a/django/views/decorators/vary.py
+++ b/django/views/decorators/vary.py
@@ -1,5 +1,7 @@
 from functools import wraps
 
+from asgiref.sync import iscoroutinefunction
+
 from django.utils.cache import patch_vary_headers
 
 
@@ -16,13 +18,21 @@ def vary_on_headers(*headers):
     """
 
     def decorator(func):
-        @wraps(func)
-        def inner_func(*args, **kwargs):
-            response = func(*args, **kwargs)
-            patch_vary_headers(response, headers)
-            return response
+        if iscoroutinefunction(func):
 
-        return inner_func
+            async def _view_wrapper(request, *args, **kwargs):
+                response = await func(request, *args, **kwargs)
+                patch_vary_headers(response, headers)
+                return response
+
+        else:
+
+            def _view_wrapper(request, *args, **kwargs):
+                response = func(request, *args, **kwargs)
+                patch_vary_headers(response, headers)
+                return response
+
+        return wraps(func)(_view_wrapper)
 
     return decorator
 
diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt
index 8884886b51..0ad0835c29 100644
--- a/docs/releases/5.0.txt
+++ b/docs/releases/5.0.txt
@@ -268,6 +268,8 @@ Decorators
   * :func:`~django.views.decorators.http.require_GET`
   * :func:`~django.views.decorators.http.require_POST`
   * :func:`~django.views.decorators.http.require_safe`
+  * :func:`~django.views.decorators.vary.vary_on_cookie`
+  * :func:`~django.views.decorators.vary.vary_on_headers`
   * ``xframe_options_deny()``
   * ``xframe_options_sameorigin()``
   * ``xframe_options_exempt()``
diff --git a/docs/topics/async.txt b/docs/topics/async.txt
index 8554e1effb..b16ffe0f78 100644
--- a/docs/topics/async.txt
+++ b/docs/topics/async.txt
@@ -94,6 +94,8 @@ view functions:
 * :func:`~django.views.decorators.http.require_GET`
 * :func:`~django.views.decorators.http.require_POST`
 * :func:`~django.views.decorators.http.require_safe`
+* :func:`~django.views.decorators.vary.vary_on_cookie`
+* :func:`~django.views.decorators.vary.vary_on_headers`
 * ``xframe_options_deny()``
 * ``xframe_options_sameorigin()``
 * ``xframe_options_exempt()``
diff --git a/docs/topics/http/decorators.txt b/docs/topics/http/decorators.txt
index 973eda72fe..38e528ecf5 100644
--- a/docs/topics/http/decorators.txt
+++ b/docs/topics/http/decorators.txt
@@ -115,6 +115,10 @@ caching based on specific request headers.
 
 .. function:: vary_on_cookie(func)
 
+    .. versionchanged:: 5.0
+
+        Support for wrapping asynchronous view functions was added.
+
 .. function:: vary_on_headers(*headers)
 
     The ``Vary`` header defines which request headers a cache mechanism should take
@@ -122,6 +126,10 @@ caching based on specific request headers.
 
     See :ref:`using vary headers <using-vary-headers>`.
 
+    .. versionchanged:: 5.0
+
+        Support for wrapping asynchronous view functions was added.
+
 .. module:: django.views.decorators.cache
 
 Caching
diff --git a/tests/decorators/test_vary.py b/tests/decorators/test_vary.py
new file mode 100644
index 0000000000..ccab18a660
--- /dev/null
+++ b/tests/decorators/test_vary.py
@@ -0,0 +1,69 @@
+from asgiref.sync import iscoroutinefunction
+
+from django.http import HttpRequest, HttpResponse
+from django.test import SimpleTestCase
+from django.views.decorators.vary import vary_on_cookie, vary_on_headers
+
+
+class VaryOnHeadersTests(SimpleTestCase):
+    def test_wrapped_sync_function_is_not_coroutine_function(self):
+        def sync_view(request):
+            return HttpResponse()
+
+        wrapped_view = vary_on_headers()(sync_view)
+        self.assertIs(iscoroutinefunction(wrapped_view), False)
+
+    def test_wrapped_async_function_is_coroutine_function(self):
+        async def async_view(request):
+            return HttpResponse()
+
+        wrapped_view = vary_on_headers()(async_view)
+        self.assertIs(iscoroutinefunction(wrapped_view), True)
+
+    def test_vary_on_headers_decorator(self):
+        @vary_on_headers("Header", "Another-header")
+        def sync_view(request):
+            return HttpResponse()
+
+        response = sync_view(HttpRequest())
+        self.assertEqual(response.get("Vary"), "Header, Another-header")
+
+    async def test_vary_on_headers_decorator_async_view(self):
+        @vary_on_headers("Header", "Another-header")
+        async def async_view(request):
+            return HttpResponse()
+
+        response = await async_view(HttpRequest())
+        self.assertEqual(response.get("Vary"), "Header, Another-header")
+
+
+class VaryOnCookieTests(SimpleTestCase):
+    def test_wrapped_sync_function_is_not_coroutine_function(self):
+        def sync_view(request):
+            return HttpResponse()
+
+        wrapped_view = vary_on_cookie(sync_view)
+        self.assertIs(iscoroutinefunction(wrapped_view), False)
+
+    def test_wrapped_async_function_is_coroutine_function(self):
+        async def async_view(request):
+            return HttpResponse()
+
+        wrapped_view = vary_on_cookie(async_view)
+        self.assertIs(iscoroutinefunction(wrapped_view), True)
+
+    def test_vary_on_cookie_decorator(self):
+        @vary_on_cookie
+        def sync_view(request):
+            return HttpResponse()
+
+        response = sync_view(HttpRequest())
+        self.assertEqual(response.get("Vary"), "Cookie")
+
+    async def test_vary_on_cookie_decorator_async_view(self):
+        @vary_on_cookie
+        async def async_view(request):
+            return HttpResponse()
+
+        response = await async_view(HttpRequest())
+        self.assertEqual(response.get("Vary"), "Cookie")