# CSRF failure as form error

**URL:** <https://forum.djangoproject.com/t/csrf-failure-as-form-error/40656>\
**Category:** Django Internals\
**Created:** [April 26, 2025, 2:38pm UTC](https://forum.djangoproject.com/t/csrf-failure-as-form-error/40656 "2025-04-26T14:38:42Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![charlesroelli](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/charlesroelli/32/13917_2.png) [@charlesroelli](https://forum.djangoproject.com/u/charlesroelli)\
**Post date:** [April 26, 2025, 2:38pm UTC](https://forum.djangoproject.com/t/csrf-failure-as-form-error/40656/1 "2025-04-26T14:38:42Z")

</div>

Form submission after logging in can result in CSRF errors for [documented reasons](https://docs.djangoproject.com/en/dev/ref/csrf/#why-might-a-user-encounter-a-csrf-validation-failure-after-logging-in), and these errors are particularly common when using short session timeouts. This can lead to data loss in certain cases. If views could instead opt-in to handle CSRF errors, allowing the error to be treated for example as a form error, the view could rerender the form with the submitted data and a refreshed CSRF token, allowing the end user to reconfirm the submission without losing what they submitted. I implemented a proof-of-concept using a decorator called `defer_csrf_failure` and a change to `FormMixin.get_form` at the DjangoCon Europe sprints today and would be interested in any feedback.

```diff
diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py
index 5ae1aae5c6..04b276beec 100644
--- a/django/middleware/csrf.py
+++ b/django/middleware/csrf.py
@@ -464,7 +464,11 @@ class CsrfViewMiddleware(MiddlewareMixin):
         try:
             self._check_token(request)
         except RejectRequest as exc:
- return self._reject(request, exc.reason)
+ if getattr(callback, "defer_csrf_failure", False):
+ request.csrf_failure_handled = False
+ request.csrf_failure_reason = exc.reason
+ else:
+ return self._reject(request, exc.reason)
 
         return self._accept(request)
 
@@ -480,4 +484,8 @@ class CsrfViewMiddleware(MiddlewareMixin):
             # by custom middleware but before those subsequent calls.
             request.META["CSRF_COOKIE_NEEDS_UPDATE"] = False
 
+ csrf_failure_handled = getattr(request, "csrf_failure_handled", None)
+ if csrf_failure_handled is False:
+ return self._reject(request, request.csrf_failure_reason)
+
         return response
diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py
index 4b8b8804b0..14ca4a39c0 100644
--- a/django/views/decorators/csrf.py
+++ b/django/views/decorators/csrf.py
@@ -67,3 +67,21 @@ def csrf_exempt(view_func):
     _view_wrapper.csrf_exempt = True
 
     return wraps(view_func)(_view_wrapper)
+
+
+def defer_csrf_failure(view_func):
+ """Allow a view function to handle CSRF errors itself."""
+
+ if iscoroutinefunction(view_func):
+
+ async def _view_wrapper(request, *args, **kwargs):
+ return await view_func(request, *args, **kwargs)
+
+ else:
+
+ def _view_wrapper(request, *args, **kwargs):
+ return view_func(request, *args, **kwargs)
+
+ _view_wrapper.defer_csrf_failure = True
+
+ return wraps(view_func)(_view_wrapper)
diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py
index ebd071cf00..cd7f09e03c 100644
--- a/django/views/generic/edit.py
+++ b/django/views/generic/edit.py
@@ -34,7 +34,13 @@ class FormMixin(ContextMixin):
         """Return an instance of the form to be used in this view."""
         if form_class is None:
             form_class = self.get_form_class()
- return form_class(**self.get_form_kwargs())
+ form = form_class(**self.get_form_kwargs())
+ if getattr(self.dispatch, "defer_csrf_failure", False) and getattr(
+ self.request, "csrf_failure_reason", None
+ ):
+ form.add_error(" __all__", self.request.csrf_failure_reason)
+ self.request.csrf_failure_handled = True
+ return form
 
     def get_form_kwargs(self):
         """Return the keyword arguments for instantiating the form."""

```
