Repository: jazzband/django-simple-history

Issue:
`HistoryRequestMiddleware` doesn't clean up after itself under some circumstances
**Describe the bug**
We caught a problem were the request was reuse, causing a strange random behaviour.

**To Reproduce**
Steps to reproduce the behavior:
- a test makes a request, so the context is updated
- an exception happens and process_response is never called, context still contains WRONG request
- the next test creates some versioned model, and the WRONG request is used

**Expected behavior**
Simple history not using the previous request.

**Environment (please complete the following information):**
 - OS: [docker python:3.8 image]
 - Django Simple History Version: 3.3.0
 - Django Version: 2.2.
 - Database Version: MySQL 8.0


Pull request here: https://github.com/jazzband/django-simple-history/pull/1188

Will complete the checklist if you think this should go in.



Discussion:

Resolving patch:
diff --git a/simple_history/middleware.py b/simple_history/middleware.py
--- a/simple_history/middleware.py
+++ b/simple_history/middleware.py
@@ -1,9 +1,7 @@
-from django.utils.deprecation import MiddlewareMixin
-
 from .models import HistoricalRecords
 
 
-class HistoryRequestMiddleware(MiddlewareMixin):
+class HistoryRequestMiddleware:
     """Expose request to HistoricalRecords.
 
     This middleware sets request as a local context/thread variable, making it
@@ -11,10 +9,15 @@ class HistoryRequestMiddleware(MiddlewareMixin):
     making a change.
     """
 
-    def process_request(self, request):
-        HistoricalRecords.context.request = request
+    def __init__(self, get_response):
+        self.get_response = get_response
 
-    def process_response(self, request, response):
-        if hasattr(HistoricalRecords.context, "request"):
+    def __call__(self, request):
+        HistoricalRecords.context.request = request
+        try:
+            response = self.get_response(request)
+        except Exception as e:
+            raise e
+        finally:
             del HistoricalRecords.context.request
         return response


Test patch:
diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py
--- a/simple_history/tests/tests/test_admin.py
+++ b/simple_history/tests/tests/test_admin.py
@@ -60,12 +60,6 @@ class AdminSiteTest(TestCase):
     def setUp(self):
         self.user = User.objects.create_superuser("user_login", "u@example.com", "pass")
 
-    def tearDown(self):
-        try:
-            del HistoricalRecords.context.request
-        except AttributeError:
-            pass
-
     def login(self, user=None, superuser=None):
         user = user or self.user
         if superuser is not None:
diff --git a/simple_history/tests/tests/test_middleware.py b/simple_history/tests/tests/test_middleware.py
--- a/simple_history/tests/tests/test_middleware.py
+++ b/simple_history/tests/tests/test_middleware.py
@@ -1,8 +1,11 @@
 from datetime import date
+from unittest import mock
 
+from django.http import HttpResponse
 from django.test import TestCase, override_settings
 from django.urls import reverse
 
+from simple_history.models import HistoricalRecords
 from simple_history.tests.custom_user.models import CustomUser
 from simple_history.tests.models import (
     BucketDataRegisterRequestUser,
@@ -146,6 +149,38 @@ def test_bucket_member_is_set_on_create_view_when_logged_in(self):
 
         self.assertListEqual([h.history_user_id for h in history], [member1.id])
 
+    # The `request` attribute of `HistoricalRecords.context` should be deleted
+    # even if this setting is set to `True`
+    @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
+    @mock.patch("simple_history.tests.view.MockableView.get")
+    def test_request_attr_is_deleted_after_each_response(self, func_mock):
+        """https://github.com/jazzband/django-simple-history/issues/1189"""
+
+        def assert_has_request_attr(has_attr: bool):
+            self.assertEqual(hasattr(HistoricalRecords.context, "request"), has_attr)
+
+        def mocked_get(*args, **kwargs):
+            assert_has_request_attr(True)
+            response_ = HttpResponse(status=200)
+            response_.historical_records_request = HistoricalRecords.context.request
+            return response_
+
+        func_mock.side_effect = mocked_get
+        self.client.force_login(self.user)
+        mockable_url = reverse("mockable")
+
+        assert_has_request_attr(False)
+        response = self.client.get(mockable_url)
+        assert_has_request_attr(False)
+        # Check that the `request` attr existed while handling the request
+        self.assertEqual(response.historical_records_request.user, self.user)
+
+        func_mock.side_effect = RuntimeError()
+        with self.assertRaises(RuntimeError):
+            self.client.get(mockable_url)
+        # The request variable should be deleted even if an exception was raised
+        assert_has_request_attr(False)
+
 
 @override_settings(**middleware_override_settings)
 class MiddlewareBulkOpsTest(TestCase):
diff --git a/simple_history/tests/urls.py b/simple_history/tests/urls.py
--- a/simple_history/tests/urls.py
+++ b/simple_history/tests/urls.py
@@ -4,6 +4,7 @@
 from simple_history.tests.view import (
     BucketDataRegisterRequestUserCreate,
     BucketDataRegisterRequestUserDetail,
+    MockableView,
     PollBulkCreateView,
     PollBulkCreateWithDefaultUserView,
     PollBulkUpdateView,
@@ -55,4 +56,5 @@
         PollBulkUpdateWithDefaultUserView.as_view(),
         name="poll-bulk-update-with-default-user",
     ),
+    path("mockable/", MockableView.as_view(), name="mockable"),
 ]
diff --git a/simple_history/tests/view.py b/simple_history/tests/view.py
--- a/simple_history/tests/view.py
+++ b/simple_history/tests/view.py
@@ -109,3 +109,10 @@ class BucketDataRegisterRequestUserCreate(CreateView):
 class BucketDataRegisterRequestUserDetail(DetailView):
     model = BucketDataRegisterRequestUser
     fields = ["data"]
+
+
+class MockableView(View):
+    """This view exists to easily mock a response."""
+
+    def get(self, request, *args, **kwargs):
+        return HttpResponse(status=200)