diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 147ae2af2a10131e80657e5fac2e9d39f2d045b6..649a2dfe0e2d8affd2c8cab443d95c422abe594f 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -15,7 +15,7 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - '3.12'
+          - '3.13'
     steps:
       - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
@@ -34,19 +34,20 @@ jobs:
           CELERY_BROKER_URL: redis://0.0.0.0:6379
           DJANGO_SETTINGS_MODULE: config.settings.test_demo_app
         run: pytest --cov=./django_structlog_demo_project --cov-append django_structlog_demo_project
-      - uses: codecov/codecov-action@v4
+      - uses: codecov/codecov-action@v5
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
+          fail_ci_if_error: false # disable for the moment because it prevents PR to succeed
   test:
     runs-on: ubuntu-latest
     strategy:
       matrix:
         python-version:
-          - '3.8'
           - '3.9'
           - '3.10'
           - '3.11'
           - '3.12'
+          - '3.13'
     steps:
       - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
@@ -58,14 +59,15 @@ jobs:
         run: |
           python -m pip install --upgrade pip
           python -m pip install -U setuptools
-          python -m pip install tox tox-gh-actions
+          python -m pip install tox tox-gh-actions -r requirements/coverage.txt
       - name: Start Redis
         uses: supercharge/redis-github-action@1.8.0
       - name: Test with tox
         run: tox
-      - uses: codecov/codecov-action@v4
+      - uses: codecov/codecov-action@v5
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
+          fail_ci_if_error: false # disable for the moment because it prevents PR to succeed
 
   test-docs:
     needs:
@@ -75,7 +77,7 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - '3.12'
+          - '3.13'
     steps:
       - uses: actions/checkout@v4
       - name: Install dependencies
@@ -97,7 +99,7 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - '3.12'
+          - '3.13'
     steps:
       - uses: actions/checkout@v4
       - name: Install dependencies
@@ -115,7 +117,7 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - '3.12'
+          - '3.13'
     steps:
       - uses: actions/checkout@v4
       - name: Install dependencies
@@ -125,13 +127,41 @@ jobs:
       - name: run ruff
         run: ruff check .
 
+  mypy:
+    needs:
+      - test-demo-app
+      - test
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version:
+          - '3.13'
+    steps:
+      - uses: actions/checkout@v4
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -r requirements/local.txt -r requirements/mypy.txt
+      - name: run mypy
+        run: mypy
+
+  isort:
+    needs:
+      - test-demo-app
+      - test
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: isort/isort-action@v1
+        with:
+            requirements-files: "requirements/local-base.txt requirements/isort.txt"
 
   test-install-base:
     runs-on: ubuntu-latest
     strategy:
       matrix:
         python-version:
-          - '3.12'
+          - '3.13'
     steps:
       - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
@@ -157,7 +187,7 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - '3.12'
+          - '3.13'
     steps:
       - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
@@ -188,7 +218,7 @@ jobs:
     strategy:
       matrix:
         python-version:
-          - '3.12'
+          - '3.13'
     steps:
       - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
@@ -220,6 +250,8 @@ jobs:
       - test-install-commands
       - black
       - ruff
+      - mypy
+      - isort
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 90f782cabfdb848223343a473a6e9d008d22260c..b3a2f7f1f3c2543f8d5c0e818584b66aa551e8a8 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -15,11 +15,11 @@ jobs:
 
       - uses: actions/setup-python@v5
         with:
-          python-version: 3.12
+          python-version: 3.13
 
       - uses: browniebroke/pre-commit-autoupdate-action@v1.0.0
 
-      - uses: peter-evans/create-pull-request@v6.0.5
+      - uses: peter-evans/create-pull-request@v7.0.6
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           branch: update/pre-commit-hooks
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3b7a2d239cece09440076423787fd8d3564ad26e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,332 @@
+### Python template
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+staticfiles/
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# Environments
+.venv
+venv/
+ENV/
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+
+### Linux template
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+
+# Provided default Pycharm Run/Debug Configurations should be tracked by git
+# In case of local modifications made by Pycharm, use update-index command
+# for each changed file, like this:
+# git update-index --assume-unchanged .idea/django_structlog_demo_project.iml
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff: 
+.idea/misc.xml
+.idea/google-java-format.xml
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Gradle:
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# CMake
+cmake-build-debug/
+
+# Mongo Explorer plugin:
+.idea/**/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+
+
+### Windows template
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+
+### macOS template
+# General
+*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+
+### SublimeText template
+# Cache files for Sublime Text
+*.tmlanguage.cache
+*.tmPreferences.cache
+*.stTheme.cache
+
+# Workspace files are user-specific
+*.sublime-workspace
+
+# Project files should be checked into the repository, unless a significant
+# proportion of contributors will probably not be using Sublime Text
+# *.sublime-project
+
+# SFTP configuration file
+sftp-config.json
+
+# Package control specific files
+Package Control.last-run
+Package Control.ca-list
+Package Control.ca-bundle
+Package Control.system-ca-bundle
+Package Control.cache/
+Package Control.ca-certs/
+Package Control.merged-ca-bundle
+Package Control.user-ca-bundle
+oscrypto-ca-bundle.crt
+bh_unicode_properties.cache
+
+# Sublime-github package stores a github token in this file
+# https://packagecontrol.io/packages/sublime-github
+GitHub.sublime-settings
+
+
+### Vim template
+# Swap
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-v][a-z]
+[._]sw[a-p]
+
+# Session
+Session.vim
+
+# Temporary
+.netrwhist
+
+# Auto-generated tag files
+tags
+
+
+### Project template
+
+django_structlog_demo_project/media/
+
+.pytest_cache/
+
+
+.ipython/
+.env
+.envs/*
+!.envs/.local/
+.vscode/
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 285d2749204b92e1708f80c817e4cbb36b38a4fa..260ec7189e1cfc0896f3858fc3f0b747dbedfed7 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -5,11 +5,11 @@
       <option name="ourVersions">
         <value>
           <list size="5">
-            <item index="0" class="java.lang.String" itemvalue="3.8" />
-            <item index="1" class="java.lang.String" itemvalue="3.9" />
-            <item index="2" class="java.lang.String" itemvalue="3.10" />
-            <item index="3" class="java.lang.String" itemvalue="3.11" />
-            <item index="4" class="java.lang.String" itemvalue="3.12" />
+            <item index="0" class="java.lang.String" itemvalue="3.9" />
+            <item index="1" class="java.lang.String" itemvalue="3.10" />
+            <item index="2" class="java.lang.String" itemvalue="3.11" />
+            <item index="3" class="java.lang.String" itemvalue="3.12" />
+            <item index="4" class="java.lang.String" itemvalue="3.13" />
           </list>
         </value>
       </option>
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 110dae9e3ca40bd249e911f7f7f86a71453bf0b0..d6a53e16530ad4e46a0cb261640e94ccadd8b4f7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,11 +1,35 @@
 repos:
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.9.3
+    hooks:
+      - id: ruff
+        args: [--fix, --exit-non-zero-on-fix]
+  - repo: https://github.com/pycqa/isort
+    rev: 5.13.2
+    hooks:
+      - id: isort
   - repo: https://github.com/ambv/black
-    rev: 24.4.2
+    rev: 24.10.0
     hooks:
       - id: black
         language_version: python3
-  - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.4.4
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v1.14.1
     hooks:
-      - id: ruff
-        args: [--fix, --exit-non-zero-on-fix]
+      - id: mypy
+        args: [--no-incremental]
+        additional_dependencies: [
+            celery-types==0.22.0,
+            "django-stubs[compatible-mypy]==5.1.2",
+            structlog==24.4.0,
+            django-extensions==3.2.3,
+            django-ipware==7.0.1,
+        ]
+        exclude: |
+          (?x)(
+            ^django_structlog_demo_project/|
+            ^config/|
+            ^docs/|
+            ^manage.py
+          )
+
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 29bad106ff60c42cc2e2fc167ae44c87e672e349..65be020c2b1a4d96fbd15a489e142d5dfc147ae7 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -7,9 +7,9 @@ version: 2
 
 # Set the version of Python and other tools you might need
 build:
-  os: ubuntu-22.04
+  os: ubuntu-24.04
   tools:
-    python: "3.12"
+    python: "3.13"
 
 # Build documentation in the docs/ directory with Sphinx
 sphinx:
diff --git a/README.rst b/README.rst
index dfb425ec45a006ba665b505e6be9edc5a97d5a2c..b32e3b1b9900ea82026052d37f1806336baaf72b 100644
--- a/README.rst
+++ b/README.rst
@@ -5,8 +5,14 @@ django-structlog
 
 | |pypi| |wheels| |build-status| |docs| |coverage| |open_issues| |pull_requests|
 | |django| |python| |license| |black| |ruff|
+| |django_packages|
 | |watchers| |stars| |forks|
 
+
+.. |django_packages| image:: https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26
+   :target: https://djangopackages.org/packages/p/django-structlog/
+   :alt: Published on Django Packages
+
 .. |build-status| image:: https://github.com/jrobichaud/django-structlog/actions/workflows/main.yml/badge.svg?branch=main
    :target: https://github.com/jrobichaud/django-structlog/actions
    :alt: Build Status
@@ -369,6 +375,44 @@ Json file (\ ``logs/json.log``\ )
 Upgrade Guide
 =============
 
+.. _upgrade_9.0:
+
+Upgrading to 9.0+
+^^^^^^^^^^^^^^^^^
+
+Minimum requirements
+~~~~~~~~~~~~~~~~~~~~
+- requires python 3.9+
+- django 4.2 and 5.0+ are supported
+
+
+Type hints
+~~~~~~~~~~
+
+``django-structlog`` now uses `python type hints <https://docs.python.org/3/library/typing.html>`_ and is being validated with `mypy <https://mypy.readthedocs.io/en/stable/>`_ ``--strict``.
+
+
+For ``drf-standardized-errors`` users
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now unhandled exceptions when using `drf-standardized-errors <https://github.com/ghazi-git/drf-standardized-errors>`_ will be intercepted and the exception logged properly.
+
+If you also use `structlog-sentry <https://github.com/kiwicom/structlog-sentry>`_, the exception will now be propagated as expected.
+
+Other libraries alike may be affected by this change.
+
+Internal changes in how ``RequestMiddleware`` handles exceptions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This only affects you if you implemented a middleware inheriting from ``RequestMiddleware`` and you overrided the ``process_exception`` method.
+
+Did you?
+
+If so:
+
+   - ``RequestMiddleware.process_exception`` was renamed to ``RequestMiddleware._process_exception``, you should to the same in the middleware.
+
+
 .. _upgrade_8.0:
 
 Upgrading to 8.0+
diff --git a/compose/local/django/Dockerfile b/compose/local/django/Dockerfile
index 62f0ec7fd00d682755e591ed8a6e238039ae2e5a..b7b6f8411f3bf78c6d64a3122000f2beeb2dedfe 100644
--- a/compose/local/django/Dockerfile
+++ b/compose/local/django/Dockerfile
@@ -10,9 +10,6 @@ ARG REQUIREMENTS_FILE=local.txt
 
 RUN <<EOF
   apk update
-  # psycopg2 dependencies
-  apk add --virtual build-deps gcc python3-dev musl-dev
-  apk add postgresql-dev
   # CFFI dependencies
   apk add libffi-dev py-cffi
   # https://docs.djangoproject.com/en/dev/ref/django-admin/#dbshell
diff --git a/compose/local/django/entrypoint b/compose/local/django/entrypoint
index 4f83411c46fc980af497e9b222c534bf4e89db2e..9255ee0fc1ec1e89cfd0ff2464a3eaac1aec9abd 100644
--- a/compose/local/django/entrypoint
+++ b/compose/local/django/entrypoint
@@ -20,17 +20,17 @@ postgres_ready() {
 python << END
 import sys
 
-import psycopg2
+import psycopg
 
 try:
-    psycopg2.connect(
+    psycopg.connect(
         dbname="${POSTGRES_DB}",
         user="${POSTGRES_USER}",
         password="${POSTGRES_PASSWORD}",
         host="${POSTGRES_HOST}",
         port="${POSTGRES_PORT}",
     )
-except psycopg2.OperationalError:
+except psycopg.OperationalError:
     sys.exit(-1)
 sys.exit(0)
 
diff --git a/compose/local/docs/start b/compose/local/docs/start
index fa672b616acc092de48699f6886cbca7a29db229..2e03361e60dc6d3b24fe37cd7ba6c0fa8f998c9a 100644
--- a/compose/local/docs/start
+++ b/compose/local/docs/start
@@ -7,7 +7,7 @@ set -o nounset
 # Basically we watch only README.rst, LICENCE.rst and everything under django_structlog
 sphinx-autobuild docs /docs/_build/html \
 -b ${SPHINX_COMMAND} \
---port 5000 \
+--port 8080 \
 --host 0.0.0.0 \
 --watch . \
 --ignore "*___jb_*" \
diff --git a/config/settings/local.py b/config/settings/local.py
index 0d9dcc6ebffc32549f686c6136e77f6f1b17edd5..aa84ba4d81e27926a95aabb5d6da2aa9847fa50c 100644
--- a/config/settings/local.py
+++ b/config/settings/local.py
@@ -1,7 +1,7 @@
 import structlog
 
 from .base import *  # noqa: F403
-from .base import env, MIDDLEWARE
+from .base import MIDDLEWARE, env
 
 # GENERAL
 # ------------------------------------------------------------------------------
diff --git a/config/settings/test.py b/config/settings/test.py
index 7d30c4d74e4311bf1f546dd960172444abadb6ff..18f288aa9293d031bf6b4c526d05b056828bc925 100644
--- a/config/settings/test.py
+++ b/config/settings/test.py
@@ -3,8 +3,8 @@ With these settings, tests run faster.
 """
 
 import os
-import environ
 
+import environ
 import structlog
 
 env = environ.Env()
diff --git a/config/urls.py b/config/urls.py
index d5be8fc7590f2a8ee4e18ede7f170da8750fed7b..4eb77072f3fee5cdaab9c11dc2da352c6816c81f 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -1,12 +1,12 @@
 from django.conf import settings
 from django.conf.urls import include
-from django.urls import re_path
 from django.conf.urls.static import static
 from django.contrib import admin
-from django.views.generic import TemplateView
+from django.urls import re_path
 from django.views import defaults as default_views
+from django.views.generic import TemplateView
 
-from django_structlog_demo_project.home import views, api_views, ninja_views
+from django_structlog_demo_project.home import api_views, ninja_views, views
 
 
 def uncaught_exception_view(request):
diff --git a/django_structlog/__init__.py b/django_structlog/__init__.py
index 0aa4ab95ca63156fbb08dd5168cd8340a2b9271c..8783fd21e83950502f209a38daf7c9bfbc95cdba 100644
--- a/django_structlog/__init__.py
+++ b/django_structlog/__init__.py
@@ -3,6 +3,6 @@
 
 name = "django_structlog"
 
-VERSION = (8, 1, 0)
+VERSION = (9, 0, 1)
 
 __version__ = ".".join(str(v) for v in VERSION)
diff --git a/django_structlog/app_settings.py b/django_structlog/app_settings.py
index e6e91e7d68654a8b641ec430ee2b5e1793232098..ab560c82369770c72aa50796d39fee2eaffedfe2 100644
--- a/django_structlog/app_settings.py
+++ b/django_structlog/app_settings.py
@@ -8,19 +8,19 @@ class AppSettings:
     PREFIX = "DJANGO_STRUCTLOG_"
 
     @property
-    def CELERY_ENABLED(self):
+    def CELERY_ENABLED(self) -> bool:
         return getattr(settings, self.PREFIX + "CELERY_ENABLED", False)
 
     @property
-    def STATUS_4XX_LOG_LEVEL(self):
+    def STATUS_4XX_LOG_LEVEL(self) -> int:
         return getattr(settings, self.PREFIX + "STATUS_4XX_LOG_LEVEL", logging.WARNING)
 
     @property
-    def COMMAND_LOGGING_ENABLED(self):
+    def COMMAND_LOGGING_ENABLED(self) -> bool:
         return getattr(settings, self.PREFIX + "COMMAND_LOGGING_ENABLED", False)
 
     @property
-    def USER_ID_FIELD(self):
+    def USER_ID_FIELD(self) -> str:
         return getattr(settings, self.PREFIX + "USER_ID_FIELD", "pk")
 
 
diff --git a/django_structlog/apps.py b/django_structlog/apps.py
index 726205a686cf24355c6e7a026c5d648778f23c9d..4fdb3689b439572f323d72f6d46336459a12f8d8 100644
--- a/django_structlog/apps.py
+++ b/django_structlog/apps.py
@@ -6,7 +6,7 @@ from .app_settings import app_settings
 class DjangoStructLogConfig(AppConfig):
     name = "django_structlog"
 
-    def ready(self):
+    def ready(self) -> None:
         if app_settings.CELERY_ENABLED:
             from .celery.receivers import CeleryReceiver
 
diff --git a/django_structlog/celery/receivers.py b/django_structlog/celery/receivers.py
index cd2f64893e6486e7c3a85c25fab04fabcec8c03f..88201fca4430c86ace2cee38f2a3830a30cee0f0 100644
--- a/django_structlog/celery/receivers.py
+++ b/django_structlog/celery/receivers.py
@@ -1,36 +1,42 @@
+from typing import TYPE_CHECKING, Any, Optional, Type, cast
+
 import structlog
 from celery import current_app
 from celery.signals import (
-    before_task_publish,
     after_task_publish,
+    before_task_publish,
+    task_failure,
     task_prerun,
+    task_rejected,
     task_retry,
-    task_success,
-    task_failure,
     task_revoked,
+    task_success,
     task_unknown,
-    task_rejected,
 )
 
 from . import signals
 
+if TYPE_CHECKING:  # pragma: no cover
+    from types import TracebackType
 
 logger = structlog.getLogger(__name__)
 
 
 class CeleryReceiver:
-    def __init__(self):
+    _priority: Optional[str]
+
+    def __init__(self) -> None:
         self._priority = None
 
     def receiver_before_task_publish(
         self,
-        sender=None,
-        headers=None,
-        body=None,
-        properties=None,
-        routing_key=None,
-        **kwargs,
-    ):
+        sender: Optional[Type[Any]] = None,
+        headers: Optional[dict[str, Any]] = None,
+        body: Optional[dict[str, str]] = None,
+        properties: Optional[dict[str, Any]] = None,
+        routing_key: Optional[str] = None,
+        **kwargs: dict[str, str],
+    ) -> None:
         if current_app.conf.task_protocol < 2:
             return
 
@@ -46,12 +52,16 @@ class CeleryReceiver:
         )
         if properties:
             self._priority = properties.get("priority", None)
-
-        headers["__django_structlog__"] = context
+        cast(dict[str, Any], headers)["__django_structlog__"] = context
 
     def receiver_after_task_publish(
-        self, sender=None, headers=None, body=None, routing_key=None, **kwargs
-    ):
+        self,
+        sender: Optional[Type[Any]] = None,
+        headers: Optional[dict[str, Optional[str]]] = None,
+        body: Optional[dict[str, Optional[str]]] = None,
+        routing_key: Optional[str] = None,
+        **kwargs: Any,
+    ) -> None:
         properties = {}
         if self._priority is not None:
             properties["priority"] = self._priority
@@ -59,13 +69,23 @@ class CeleryReceiver:
 
         logger.info(
             "task_enqueued",
-            child_task_id=headers.get("id") if headers else body.get("id"),
-            child_task_name=headers.get("task") if headers else body.get("task"),
+            child_task_id=(
+                headers.get("id")
+                if headers
+                else cast(dict[str, Optional[str]], body).get("id")
+            ),
+            child_task_name=(
+                headers.get("task")
+                if headers
+                else cast(dict[str, Optional[str]], body).get("task")
+            ),
             routing_key=routing_key,
             **properties,
         )
 
-    def receiver_task_prerun(self, task_id, task, *args, **kwargs):
+    def receiver_task_prerun(
+        self, task_id: str, task: Any, *args: Any, **kwargs: Any
+    ) -> None:
         structlog.contextvars.clear_contextvars()
         structlog.contextvars.bind_contextvars(task_id=task_id)
         metadata = getattr(task.request, "__django_structlog__", {})
@@ -75,10 +95,18 @@ class CeleryReceiver:
         )
         logger.info("task_started", task=task.name)
 
-    def receiver_task_retry(self, request=None, reason=None, einfo=None, **kwargs):
+    def receiver_task_retry(
+        self,
+        request: Optional[Any] = None,
+        reason: Optional[str] = None,
+        einfo: Optional[Any] = None,
+        **kwargs: Any,
+    ) -> None:
         logger.warning("task_retrying", reason=reason)
 
-    def receiver_task_success(self, result=None, **kwargs):
+    def receiver_task_success(
+        self, result: Optional[str] = None, **kwargs: Any
+    ) -> None:
         signals.pre_task_succeeded.send(
             sender=self.receiver_task_success, logger=logger, result=result
         )
@@ -86,14 +114,14 @@ class CeleryReceiver:
 
     def receiver_task_failure(
         self,
-        task_id=None,
-        exception=None,
-        traceback=None,
-        einfo=None,
-        sender=None,
-        *args,
-        **kwargs,
-    ):
+        task_id: Optional[str] = None,
+        exception: Optional[Exception] = None,
+        traceback: Optional["TracebackType"] = None,
+        einfo: Optional[Any] = None,
+        sender: Optional[Type[Any]] = None,
+        *args: Any,
+        **kwargs: Any,
+    ) -> None:
         throws = getattr(sender, "throws", ())
         if isinstance(exception, throws):
             logger.info(
@@ -108,8 +136,13 @@ class CeleryReceiver:
             )
 
     def receiver_task_revoked(
-        self, request=None, terminated=None, signum=None, expired=None, **kwargs
-    ):
+        self,
+        request: Any,
+        terminated: Optional[bool] = None,
+        signum: Optional[Any] = None,
+        expired: Optional[Any] = None,
+        **kwargs: Any,
+    ) -> None:
         metadata = getattr(request, "__django_structlog__", {}).copy()
         metadata["task_id"] = request.id
         metadata["task"] = request.task
@@ -124,24 +157,31 @@ class CeleryReceiver:
         )
 
     def receiver_task_unknown(
-        self, message=None, exc=None, name=None, id=None, **kwargs
-    ):
+        self,
+        message: Optional[str] = None,
+        exc: Optional[Exception] = None,
+        name: Optional[str] = None,
+        id: Optional[str] = None,
+        **kwargs: Any,
+    ) -> None:
         logger.error(
             "task_not_found",
             task=name,
             task_id=id,
         )
 
-    def receiver_task_rejected(self, message=None, exc=None, **kwargs):
+    def receiver_task_rejected(
+        self, message: Any, exc: Optional[Exception] = None, **kwargs: Any
+    ) -> None:
         logger.exception(
             "task_rejected", task_id=message.properties.get("correlation_id")
         )
 
-    def connect_signals(self):
+    def connect_signals(self) -> None:
         before_task_publish.connect(self.receiver_before_task_publish)
         after_task_publish.connect(self.receiver_after_task_publish)
 
-    def connect_worker_signals(self):
+    def connect_worker_signals(self) -> None:
         before_task_publish.connect(self.receiver_before_task_publish)
         after_task_publish.connect(self.receiver_after_task_publish)
         task_prerun.connect(self.receiver_task_prerun)
diff --git a/django_structlog/celery/signals.py b/django_structlog/celery/signals.py
index efa1b4532f88fe37c6f882c539d5cdac05920a64..f01b3291e56f2fa6a06dca60c2afed90b9328fea 100644
--- a/django_structlog/celery/signals.py
+++ b/django_structlog/celery/signals.py
@@ -1,6 +1,5 @@
 import django.dispatch
 
-
 bind_extra_task_metadata = django.dispatch.Signal()
 """ Signal to add extra ``structlog`` bindings from ``celery``'s task.
 
diff --git a/django_structlog/celery/steps.py b/django_structlog/celery/steps.py
index 9deb4a20c07055fa98e3b4c363f109c283fd2500..51b1a4e76e10d68e7a65c90a87e3a02cbe0c9458 100644
--- a/django_structlog/celery/steps.py
+++ b/django_structlog/celery/steps.py
@@ -1,3 +1,5 @@
+from typing import Any
+
 from celery import bootsteps
 
 from .receivers import CeleryReceiver
@@ -14,7 +16,7 @@ class DjangoStructLogInitStep(bootsteps.Step):
 
     """
 
-    def __init__(self, parent, **kwargs):
+    def __init__(self, parent: Any, **kwargs: Any) -> None:
         super().__init__(parent, **kwargs)
         self.receiver = CeleryReceiver()
         self.receiver.connect_worker_signals()
diff --git a/django_structlog/commands.py b/django_structlog/commands.py
index 85b088c5bc27c1c32ad5725567ccf96924c19d4f..8ffca16a8ff9beeec186d9ac394e316608b645a2 100644
--- a/django_structlog/commands.py
+++ b/django_structlog/commands.py
@@ -1,16 +1,25 @@
-import structlog
 import uuid
+from typing import TYPE_CHECKING, Any, List, Mapping, Tuple, Type
+
+import structlog
+from django_extensions.management.signals import (  # type: ignore[import-untyped]
+    post_command,
+    pre_command,
+)
 
-from django_extensions.management.signals import pre_command, post_command
+if TYPE_CHECKING:  # pragma: no cover
+    import contextvars
 
 logger = structlog.getLogger(__name__)
 
 
 class DjangoCommandReceiver:
-    def __init__(self):
+    stack: List[Tuple[str, Mapping[str, "contextvars.Token[Any]"]]]
+
+    def __init__(self) -> None:
         self.stack = []
 
-    def pre_receiver(self, sender, *args, **kwargs):
+    def pre_receiver(self, sender: Type[Any], *args: Any, **kwargs: Any) -> None:
         command_id = str(uuid.uuid4())
         if len(self.stack):
             parent_command_id, _ = self.stack[-1]
@@ -26,13 +35,15 @@ class DjangoCommandReceiver:
             command_name=sender.__module__.replace(".management.commands", ""),
         )
 
-    def post_receiver(self, sender, outcome, *args, **kwargs):
+    def post_receiver(
+        self, sender: Type[Any], outcome: str, *args: Any, **kwargs: Any
+    ) -> None:
         logger.info("command_finished")
 
         if len(self.stack):  # pragma: no branch
             command_id, tokens = self.stack.pop()
             structlog.contextvars.reset_contextvars(**tokens)
 
-    def connect_signals(self):
+    def connect_signals(self) -> None:
         pre_command.connect(self.pre_receiver)
         post_command.connect(self.post_receiver)
diff --git a/django_structlog/middlewares/__init__.py b/django_structlog/middlewares/__init__.py
index 55da6f6b39443ef362e256c13e034de9372e251a..d9d9b02e13d8a07b7dd4f23a6514674692e7f6f7 100644
--- a/django_structlog/middlewares/__init__.py
+++ b/django_structlog/middlewares/__init__.py
@@ -1 +1,5 @@
 from .request import RequestMiddleware  # noqa F401
+
+__all__ = [
+    "RequestMiddleware",
+]
diff --git a/django_structlog/middlewares/request.py b/django_structlog/middlewares/request.py
index f17a7be17f8a907339b68c66b266aa9931cb137b..e0ac48d140d4e28683da9c6e475e6777c6330652 100644
--- a/django_structlog/middlewares/request.py
+++ b/django_structlog/middlewares/request.py
@@ -1,27 +1,59 @@
 import asyncio
 import logging
+import sys
 import uuid
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    AsyncGenerator,
+    AsyncIterator,
+    Awaitable,
+    Callable,
+    Generator,
+    Iterator,
+    Type,
+    Union,
+    cast,
+)
 
 import structlog
-from asgiref.sync import iscoroutinefunction, markcoroutinefunction
+from asgiref import sync
 from django.core.exceptions import PermissionDenied
+from django.core.signals import got_request_exception
 from django.http import Http404, StreamingHttpResponse
-from asgiref import sync
 
 from .. import signals
 from ..app_settings import app_settings
 
+if sys.version_info >= (3, 12, 0):
+    from inspect import (  # type: ignore[attr-defined]
+        iscoroutinefunction,
+        markcoroutinefunction,
+    )
+else:
+    from asgiref.sync import (  # type: ignore[no-redef]
+        iscoroutinefunction,
+        markcoroutinefunction,
+    )
+
+if TYPE_CHECKING:  # pragma: no cover
+    from types import TracebackType
+
+    from django.http import HttpRequest, HttpResponse
+
 logger = structlog.getLogger(__name__)
 
 
-def get_request_header(request, header_key, meta_key):
+def get_request_header(request: "HttpRequest", header_key: str, meta_key: str) -> Any:
     if hasattr(request, "headers"):
         return request.headers.get(header_key)
 
     return request.META.get(meta_key)
 
 
-def sync_streaming_content_wrapper(streaming_content, context):
+def sync_streaming_content_wrapper(
+    streaming_content: Iterator[bytes], context: Any
+) -> Generator[bytes, None, None]:
     with structlog.contextvars.bound_contextvars(**context):
         logger.info("streaming_started")
         try:
@@ -29,11 +61,14 @@ def sync_streaming_content_wrapper(streaming_content, context):
                 yield chunk
         except Exception:
             logger.exception("streaming_failed")
+            raise
         else:
             logger.info("streaming_finished")
 
 
-async def async_streaming_content_wrapper(streaming_content, context):
+async def async_streaming_content_wrapper(
+    streaming_content: AsyncIterator[bytes], context: Any
+) -> AsyncGenerator[bytes, Any]:
     with structlog.contextvars.bound_contextvars(**context):
         logger.info("streaming_started")
         try:
@@ -44,6 +79,7 @@ async def async_streaming_content_wrapper(streaming_content, context):
             raise
         except Exception:
             logger.exception("streaming_failed")
+            raise
         else:
             logger.info("streaming_finished")
 
@@ -61,30 +97,38 @@ class RequestMiddleware:
     sync_capable = True
     async_capable = True
 
-    def __init__(self, get_response):
+    def __init__(
+        self,
+        get_response: Callable[
+            ["HttpRequest"], Union["HttpResponse", Awaitable["HttpResponse"]]
+        ],
+    ) -> None:
         self.get_response = get_response
         if iscoroutinefunction(self.get_response):
             markcoroutinefunction(self)
+        got_request_exception.connect(self.process_got_request_exception)
 
-    def __call__(self, request):
+    def __call__(
+        self, request: "HttpRequest"
+    ) -> Union["HttpResponse", Awaitable["HttpResponse"]]:
         if iscoroutinefunction(self):
-            return self.__acall__(request)
+            return cast(RequestMiddleware, self).__acall__(request)
         self.prepare(request)
-        response = self.get_response(request)
+        response = cast("HttpResponse", self.get_response(request))
         self.handle_response(request, response)
         return response
 
-    async def __acall__(self, request):
+    async def __acall__(self, request: "HttpRequest") -> "HttpResponse":
         await sync.sync_to_async(self.prepare)(request)
         try:
-            response = await self.get_response(request)
+            response = await cast(Awaitable["HttpResponse"], self.get_response(request))
         except asyncio.CancelledError:
             logger.warning("request_cancelled")
             raise
         await sync.sync_to_async(self.handle_response)(request, response)
         return response
 
-    def handle_response(self, request, response):
+    def handle_response(self, request: "HttpRequest", response: "HttpResponse") -> None:
         if not hasattr(request, "_raised_exception"):
             self.bind_user_id(request)
             context = structlog.contextvars.get_merged_contextvars(logger)
@@ -113,15 +157,13 @@ class RequestMiddleware:
             )
             if isinstance(response, StreamingHttpResponse):
                 streaming_content = response.streaming_content
-                try:
-                    iter(streaming_content)
-                except TypeError:
+                if response.is_async:
                     response.streaming_content = async_streaming_content_wrapper(
-                        streaming_content, context
+                        cast(AsyncIterator[bytes], streaming_content), context
                     )
                 else:
                     response.streaming_content = sync_streaming_content_wrapper(
-                        streaming_content, context
+                        cast(Iterator[bytes], streaming_content), context
                     )
 
         else:
@@ -136,8 +178,8 @@ class RequestMiddleware:
             )
         structlog.contextvars.clear_contextvars()
 
-    def prepare(self, request):
-        from ipware import get_client_ip
+    def prepare(self, request: "HttpRequest") -> None:
+        from ipware import get_client_ip  # type: ignore[import-untyped]
 
         request_id = get_request_header(
             request, "x-request-id", "HTTP_X_REQUEST_ID"
@@ -161,11 +203,11 @@ class RequestMiddleware:
         logger.info("request_started", **log_kwargs)
 
     @staticmethod
-    def format_request(request):
+    def format_request(request: "HttpRequest") -> str:
         return f"{request.method} {request.get_full_path()}"
 
     @staticmethod
-    def bind_user_id(request):
+    def bind_user_id(request: "HttpRequest") -> None:
         user_id_field = app_settings.USER_ID_FIELD
         if hasattr(request, "user") and request.user is not None and user_id_field:
             user_id = None
@@ -175,7 +217,17 @@ class RequestMiddleware:
                     user_id = str(user_id)
             structlog.contextvars.bind_contextvars(user_id=user_id)
 
-    def process_exception(self, request, exception):
+    def process_got_request_exception(
+        self, sender: Type[Any], request: "HttpRequest", **kwargs: Any
+    ) -> None:
+        if not hasattr(request, "_raised_exception"):
+            ex = cast(
+                tuple[Type[Exception], Exception, "TracebackType"],
+                sys.exc_info(),
+            )
+            self._process_exception(request, ex[1])
+
+    def _process_exception(self, request: "HttpRequest", exception: Exception) -> None:
         if isinstance(exception, (Http404, PermissionDenied)):
             # We don't log an exception here, and we don't set that we handled
             # an error as we want the standard `request_finished` log message
diff --git a/django_structlog/py.typed b/django_structlog/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/django_structlog/signals.py b/django_structlog/signals.py
index 2a08e14366266329ac985e53cc20666929f5ac85..9893f4401a4db0e4b4abd9531a1af742435baff5 100644
--- a/django_structlog/signals.py
+++ b/django_structlog/signals.py
@@ -1,6 +1,5 @@
 import django.dispatch
 
-
 bind_extra_request_metadata = django.dispatch.Signal()
 """ Signal to add extra ``structlog`` bindings from ``django``'s request.
 
diff --git a/django_structlog_demo_project/home/api_views.py b/django_structlog_demo_project/home/api_views.py
index 857bc74e97f5d960a5231d68b392fdb2832c63e7..4720899464ab98336f820a5be4ed1b7f6ae2e345 100644
--- a/django_structlog_demo_project/home/api_views.py
+++ b/django_structlog_demo_project/home/api_views.py
@@ -1,6 +1,6 @@
+import structlog
 from rest_framework.decorators import api_view
 from rest_framework.response import Response
-import structlog
 
 logger = structlog.get_logger(__name__)
 
diff --git a/django_structlog_demo_project/home/ninja_views.py b/django_structlog_demo_project/home/ninja_views.py
index 31c19a5731e16ef545273a81b536ed9b65dea257..e37dfcd52dd3329dba0cd1efbb04120b73612929 100644
--- a/django_structlog_demo_project/home/ninja_views.py
+++ b/django_structlog_demo_project/home/ninja_views.py
@@ -14,7 +14,7 @@ class OptionalSessionAuth(SessionAuth):
         return request.user
 
 
-@router.get("/ninja", url_name="add", auth=OptionalSessionAuth())
+@router.get("/ninja", url_name="ninja", auth=OptionalSessionAuth())
 def ninja(request):
     logger.info("This is a ninja structured log")
     return {"result": "ok"}
diff --git a/django_structlog_demo_project/home/views.py b/django_structlog_demo_project/home/views.py
index 5ec4d0ffd6fadc14ada5d888ce063ae7bd3e2bd9..4dc746a7a543f0346913de316f4a0493575c06f5 100644
--- a/django_structlog_demo_project/home/views.py
+++ b/django_structlog_demo_project/home/views.py
@@ -4,11 +4,12 @@ import time
 
 import structlog
 from django.http import HttpResponse, StreamingHttpResponse
+
 from django_structlog_demo_project.taskapp.celery import (
-    successful_task,
     failing_task,
     nesting_task,
     rejected_task,
+    successful_task,
 )
 
 logger = structlog.get_logger(__name__)
@@ -44,9 +45,7 @@ def revoke_task(request):
 
 
 def enqueue_unknown_task(request):
-    from django_structlog_demo_project.taskapp.celery import (
-        unknown_task,
-    )
+    from django_structlog_demo_project.taskapp.celery import unknown_task
 
     logger.info("Enqueuing unknown task")
     unknown_task.delay()
diff --git a/django_structlog_demo_project/taskapp/celery.py b/django_structlog_demo_project/taskapp/celery.py
index b1d964e00817250b837f9731d77ed89439926590..b946fb8fcbf37815bf1478f4397360c39b7164ba 100644
--- a/django_structlog_demo_project/taskapp/celery.py
+++ b/django_structlog_demo_project/taskapp/celery.py
@@ -4,7 +4,7 @@ import os
 
 import structlog
 from celery import Celery, shared_task, signals
-from django.apps import apps, AppConfig
+from django.apps import AppConfig, apps
 from django.conf import settings
 
 from django_structlog.celery.steps import DjangoStructLogInitStep
diff --git a/django_structlog_demo_project/users/forms.py b/django_structlog_demo_project/users/forms.py
index 94a3b450ec583b5796399e24474b09685f2a2e25..cf4b25a56ab4ee6cf08469072c118202306f9486 100644
--- a/django_structlog_demo_project/users/forms.py
+++ b/django_structlog_demo_project/users/forms.py
@@ -1,4 +1,4 @@
-from django.contrib.auth import get_user_model, forms
+from django.contrib.auth import forms, get_user_model
 from django.core.exceptions import ValidationError
 from django.utils.translation import gettext_lazy as _
 
diff --git a/django_structlog_demo_project/users/migrations/0001_initial.py b/django_structlog_demo_project/users/migrations/0001_initial.py
index 8176245b4466ea2066fb2266f115b5049a8d131f..4d9976d64d8783a03cd4de62d2be85610a2067e1 100644
--- a/django_structlog_demo_project/users/migrations/0001_initial.py
+++ b/django_structlog_demo_project/users/migrations/0001_initial.py
@@ -1,7 +1,7 @@
 import django.contrib.auth.models
 import django.contrib.auth.validators
-from django.db import migrations, models
 import django.utils.timezone
+from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
diff --git a/django_structlog_demo_project/users/tests/test_adapters.py b/django_structlog_demo_project/users/tests/test_adapters.py
index 3f611b55b655ce700da113a5d9ba7ca5c902ef8d..44c04db0b6a05ce4e735e3e2ad4db14389d391c2 100644
--- a/django_structlog_demo_project/users/tests/test_adapters.py
+++ b/django_structlog_demo_project/users/tests/test_adapters.py
@@ -1,8 +1,6 @@
 import pytest
 
-from django_structlog_demo_project.users.adapters import (
-    AccountAdapter,
-)
+from django_structlog_demo_project.users.adapters import AccountAdapter
 
 pytestmark = pytest.mark.django_db
 
diff --git a/django_structlog_demo_project/users/tests/test_urls.py b/django_structlog_demo_project/users/tests/test_urls.py
index 2191d12d50eda68b186ba9bfe3c33485ea2b6734..17bb2a407542302c256d7df7828dd2e3e6f36714 100644
--- a/django_structlog_demo_project/users/tests/test_urls.py
+++ b/django_structlog_demo_project/users/tests/test_urls.py
@@ -1,6 +1,6 @@
 import pytest
 from django.conf import settings
-from django.urls import reverse, resolve
+from django.urls import resolve, reverse
 
 pytestmark = pytest.mark.django_db
 
diff --git a/django_structlog_demo_project/users/urls.py b/django_structlog_demo_project/users/urls.py
index ce5351379f1c71313c7c55a1cdb2a6819ae78b6b..3b2c4ac6d1f7700cb51c1b8d0cf008ca282c680e 100644
--- a/django_structlog_demo_project/users/urls.py
+++ b/django_structlog_demo_project/users/urls.py
@@ -1,10 +1,10 @@
 from django.urls import re_path
 
 from django_structlog_demo_project.users.views import (
+    user_detail_view,
     user_list_view,
     user_redirect_view,
     user_update_view,
-    user_detail_view,
 )
 
 app_name = "users"
diff --git a/docker-compose.docs.yml b/docker-compose.docs.yml
index e50313566acbf3331ca3c77e096468443e4ec09d..dbb67375f53e62fa6b7ecf34a790f497b47c7b66 100644
--- a/docker-compose.docs.yml
+++ b/docker-compose.docs.yml
@@ -4,7 +4,7 @@ services:
       context: .
       dockerfile: ./compose/local/docs/Dockerfile
       args:
-        PYTHON_VERSION: 3.12
+        PYTHON_VERSION: 3.13
     image: django_structlog_demo_project_docs
     volumes:
       - .:/app:cached
@@ -12,7 +12,7 @@ services:
     environment:
       - SPHINX_COMMAND=html
     ports:
-      - "5000:5000"
+      - "8080:8080"
   docs-test:
     image: django_structlog_demo_project_docs
     volumes:
diff --git a/docker-compose.yml b/docker-compose.yml
index 6931e5cf7ef468f75441e8998d3ac4fe156f359a..c6a90b00545be500d71b410f78e33977e9c60ab6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,7 +8,7 @@ services:
       context: .
       dockerfile: ./compose/local/django/Dockerfile
       args:
-        PYTHON_VERSION: 3.12
+        PYTHON_VERSION: 3.13
     image: django_structlog_demo_project_local_django
     depends_on:
       - postgres
diff --git a/docs/changelog.rst b/docs/changelog.rst
index e1b9012bf85d6afef61928cd96104cec375fde9d..88035702eca9b9b19506312ebbf42c100479c2d3 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,11 +1,38 @@
 Change Log
 ==========
 
+9.0.1 (January 29, 2024)
+------------------------
+
+*Fixes:*
+    - Fix exceptions not being propagated when using streaming response. See `#747 <https://github.com/jrobichaud/django-structlog/pull/747>`_. Special thanks to `@liambuchanan <https://github.com/liambuchanan>`_.
+
+
+9.0.0 (November 26, 2024)
+-------------------------
+
+See: :ref:`upgrade_9.0`
+
+*New:*
+    - Add type definitions for the project. See `#697 <https://github.com/jrobichaud/django-structlog/pull/697>`_ and `#696 <https://github.com/jrobichaud/django-structlog/issues/696>`_. Special thanks to `@j00bar <https://github.com/j00bar>`_ and `@MaxDude132 <https://github.com/MaxDude132>`_ for the review.
+
+*Changes:*
+    - ``RequestMiddleware`` now relies on django signal `got_request_exception <https://docs.djangoproject.com/en/dev/ref/signals/#got-request-exception>`_ instead of Middleware `process_exception <https://docs.djangoproject.com/en/dev/topics/http/middleware/#process-exception>`_ method. See `#705 <https://github.com/jrobichaud/django-structlog/pull/705>`_, `#658 <https://github.com/jrobichaud/django-structlog/issues/658>`_ and :ref:`upgrade_9.0`. Special thanks to `@sshishov <https://github.com/sshishov>`_.
+    - Add python 3.13 support. See `#674 <https://github.com/jrobichaud/django-structlog/pull/674>`_.
+    - Drop python 3.8 support. See `#674 <https://github.com/jrobichaud/django-structlog/pull/674>`_.
+    - Django 5.1 and celery 5.4 support. See `#617 <https://github.com/jrobichaud/django-structlog/pull/617>`_.
+
+*Other:*
+    - Migrated project to use python 3.13 along with readthedocs generation.
+    - now use `isort <https://pycqa.github.io/isort/>`_
+    - fixed ``codecov`` github action that was not properly configured and therefore not properly reporting coverage.
+
+
 8.1.0 (May 24, 2024)
 --------------------
 
 *New:*
-    - Add a :ref:`setting <settings>` ``DJANGO_STRUCTLOG_USER_ID_FIELD = 'pk'`` to customize what user field to use as ``user_id`` in the logs. `#546 <https://github.com/jrobichaud/django-structlog/pull/546>`_ and `#545 <https://github.com/jrobichaud/django-structlog/issues/545>`_. Special thanks to `@sshishov <https://github.com/ sshishov>`_.
+    - Add a :ref:`setting <settings>` ``DJANGO_STRUCTLOG_USER_ID_FIELD = 'pk'`` to customize what user field to use as ``user_id`` in the logs. See `#546 <https://github.com/jrobichaud/django-structlog/pull/546>`_ and `#545 <https://github.com/jrobichaud/django-structlog/issues/545>`_. Special thanks to `@sshishov <https://github.com/sshishov>`_.
 
 *Changes:*
     - Drop support of python 3.7
diff --git a/docs/conf.py b/docs/conf.py
index cda928847d05ef1806ac522b2e66c5fed2030c18..d024c18e10eabd4aaa92a63c78623db7d49ccf5e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,5 +1,5 @@
-import sys
 import os
+import sys
 
 sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
 
diff --git a/docs/development.rst b/docs/development.rst
index 11bc9e731b6c91cff12846e7d74fedafefc65b65..5874137352a27929d4ba416eec4ff64374e8b0e0 100644
--- a/docs/development.rst
+++ b/docs/development.rst
@@ -45,4 +45,4 @@ Building, Serving and Testing the Documentation Locally
 .. code-block:: bash
 
    $ docker compose -p django-structlog-docs -f docker-compose.docs.yml up --build
-   Serving on http://127.0.0.1:5000
+   Serving on http://127.0.0.1:8080
diff --git a/docs/how_tos.rst b/docs/how_tos.rst
index ab1652f2d08c9774049b7bbcd6cab9a8de58cdcb..c581ba3ffbbeb7e3753598638813621eee2480ca 100644
--- a/docs/how_tos.rst
+++ b/docs/how_tos.rst
@@ -100,7 +100,7 @@ Origin: `#412 <https://github.com/jrobichaud/django-structlog/issues/412>`_
             self.excluded_event_type = excluded_event_type
 
         def filter(self, record):
-            if not isinstance(msg, dict) or self.excluded_event_type is None:
+            if not isinstance(record.msg, dict) or self.excluded_event_type is None:
                 return True  # Include the log message if msg is not a dictionary or excluded_event_type is not provided
 
             if record.msg.get('event') in self.excluded_event_type:
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 1764f04f4e649017828eb6fb2dad269122802ce5..b97bbcadea06e91efd2fdb78aec3064b38e7ef31 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,8 +1,8 @@
-sphinx==7.3.7
-sphinx_rtd_theme==2.0.0
+sphinx==8.1.3
+sphinx_rtd_theme==3.0.2
 celery==5.4.0
 django>=4.2,<6
 structlog
-sphinx-autobuild==2024.4.16
-Jinja2==3.1.4
-importlib-metadata>=1,<8
+sphinx-autobuild==2024.10.3
+Jinja2==3.1.5
+importlib-metadata>=8.0.0,<9
diff --git a/pyproject.toml b/pyproject.toml
index e0d8cc60ae1bf7c6516a6bf1670cfa0aeb42a449..d97440c780b032c2b4034a9d2d3b220dd5a74f04 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
   ]
   readme = "README.rst"
   dynamic = ["version"]
-  requires-python = ">=3.8"
+  requires-python = ">=3.9"
   license = { text = "MIT" }
   dependencies = [
     "django>=4.2",
@@ -23,16 +23,18 @@ build-backend = "setuptools.build_meta"
     "Framework :: Django",
     "Framework :: Django :: 4.2",
     "Framework :: Django :: 5.0",
+    "Framework :: Django :: 5.1",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3 :: Only",
-    "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
     "Topic :: System :: Logging",
     "License :: OSI Approved :: MIT License",
     "Operating System :: OS Independent",
+    "Typing :: Typed",
   ]
 
 [project.urls]
@@ -62,11 +64,11 @@ build-backend = "setuptools.build_meta"
 [tool.black]
   line-length = 88
   target-version = [
-    'py38',
     'py39',
     'py310',
     'py311',
     'py312',
+    'py313',
   ]
   include = '\.pyi?$'
   exclude = '''
@@ -84,7 +86,7 @@ build-backend = "setuptools.build_meta"
 
 [tool.ruff]
   line-length = 88
-  target-version = "py312"
+  target-version = "py313"
   lint.ignore = [
     'E501',
   ]
@@ -99,17 +101,18 @@ build-backend = "setuptools.build_meta"
     #
     # Also, make sure that all python versions used here are included in ./github/worksflows/main.yml
     envlist =
-        py{38,39,310,311}-django42-celery5{2,3}-redis{3,4}-kombu5,
-        py31{0,1}-django50-celery53-redis4-kombu5,
-        py312-django{42,50}-celery53-redis4-kombu5,
+        py{39,310,311}-django42-celery5{2,3}-redis{3,4}-kombu5,
+        py31{0,1}-django5{0,1}-celery5{3,4}-redis4-kombu5,
+        py312-django{42,50,51}-celery5{3,4}-redis4-kombu5,
+        py313-django{51}-celery5{3,4}-redis4-kombu5,
 
     [gh-actions]
     python =
-        3.8: py38
         3.9: py39
         3.10: py310
         3.11: py311
         3.12: py312
+        3.13: py313
 
     [testenv]
     setenv =
@@ -125,8 +128,10 @@ build-backend = "setuptools.build_meta"
         celery51: Celery >=5.1, <5.2
         celery52: Celery >=5.2, <5.3
         celery53: Celery >=5.3, <5.4
+        celery54: Celery >=5.4, <5.5
         django42: Django >=4.2, <5.0
         django50: Django >=5.0, <5.1
+        django51: Django >=5.1, <5.2
         -r{toxinidir}/requirements/ci.txt
 
     commands = pytest --cov=./test_app --cov=./django_structlog --cov-append test_app
@@ -148,3 +153,15 @@ include = [
   "./django_structlog_demo_project/*",
   "./test_app/*",
 ]
+
+[tool.mypy]
+python_version=3.9
+strict=true
+packages=[
+  "django_structlog",
+  "test_app",
+]
+
+[tool.isort]
+profile = "black"
+filter_files = true
diff --git a/requirements/black.txt b/requirements/black.txt
index 033748b7e14523e59bd447c98a6e0547bd0fe6e6..dfa81c4901875f872810da7062211c0cea0953e4 100644
--- a/requirements/black.txt
+++ b/requirements/black.txt
@@ -1 +1 @@
-black==24.4.2  # https://github.com/ambv/black
+black==24.10.0  # https://github.com/ambv/black
diff --git a/requirements/ci.txt b/requirements/ci.txt
index 4abc02d4f3eeb5b015ad0cfb76b2a515f2c5b4cc..58cc38a38d9ade43eb62ad83940edc620b9b742f 100644
--- a/requirements/ci.txt
+++ b/requirements/ci.txt
@@ -1,32 +1,32 @@
 # Django
 # ------------------------------------------------------------------------------
-django-environ==0.11.2  # https://github.com/joke2k/django-environ
+django-environ==0.12.0  # https://github.com/joke2k/django-environ
 django-redis==5.4.0  # https://github.com/niwinz/django-redis
 django-extensions==3.2.3
 
 structlog>=21.4.0
 colorama>=0.4.3
 
-psycopg2-binary==2.9.9  # https://github.com/psycopg/psycopg2
+psycopg[binary]==3.2.4 # https://github.com/psycopg/psycopg
 
 # Testing
 # ------------------------------------------------------------------------------
-pytest==8.2.1   # https://github.com/pytest-dev/pytest
+pytest==8.3.4   # https://github.com/pytest-dev/pytest
 pytest-sugar==1.0.0  # https://github.com/Frozenball/pytest-sugar
-pytest-cov==5.0.0
+pytest-cov==6.0.0
 
 # Code quality
 # ------------------------------------------------------------------------------
-coverage==7.5.1  # https://github.com/nedbat/coveragepy
-pylint-django==2.5.5  # https://github.com/PyCQA/pylint-django
+-r coverage.txt
+pylint-django==2.6.1  # https://github.com/PyCQA/pylint-django
 pylint-celery==0.3  # https://github.com/PyCQA/pylint-celery
 
 # Django
 # ------------------------------------------------------------------------------
-factory-boy==3.3.0  # https://github.com/FactoryBoy/factory_boy
+factory-boy==3.3.1  # https://github.com/FactoryBoy/factory_boy
 
 django-coverage-plugin==3.1.0  # https://github.com/nedbat/django_coverage_plugin
-pytest-django==4.8.0  # https://github.com/pytest-dev/pytest-django
+pytest-django==4.9.0  # https://github.com/pytest-dev/pytest-django
 
 # Setup tools
 # ------------------------------------------------------------------------------
diff --git a/requirements/coverage.txt b/requirements/coverage.txt
new file mode 100644
index 0000000000000000000000000000000000000000..396b98ac97df905ba61e42acbb44ce3eafd07766
--- /dev/null
+++ b/requirements/coverage.txt
@@ -0,0 +1 @@
+coverage==7.6.10  # https://github.com/nedbat/coveragepy
diff --git a/requirements/deployment.txt b/requirements/deployment.txt
index 7872cbbd8b949c8ac70bfe5574b9a7e87c587b6a..a358e18d996f7524c728088684a499a0d410b6c1 100644
--- a/requirements/deployment.txt
+++ b/requirements/deployment.txt
@@ -1 +1 @@
-importlib-metadata>=1,<8
+importlib-metadata>=8.0.0,<9
diff --git a/requirements/isort.txt b/requirements/isort.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/requirements/local-base.txt b/requirements/local-base.txt
index 483a99dc9f40ac795dbc988709c48aea56afc593..126adecdfccfc446128e2434c6001f2ee2c2dab4 100644
--- a/requirements/local-base.txt
+++ b/requirements/local-base.txt
@@ -1,56 +1,56 @@
-pytz==2024.1  # https://github.com/stub42/pytz
+pytz==2024.2  # https://github.com/stub42/pytz
 python-slugify==8.0.4  # https://github.com/un33k/python-slugify
 
 # Django
 # ------------------------------------------------------------------------------
-django==5.0.6  # https://www.djangoproject.com/
-django-environ==0.11.2  # https://github.com/joke2k/django-environ
-django-model-utils==4.5.1  # https://github.com/jazzband/django-model-utils
-django-allauth==0.63.1  # https://github.com/pennersr/django-allauth
-django-crispy-forms==2.1  # https://github.com/django-crispy-forms/django-crispy-forms
-crispy-bootstrap5==2024.2 # https://github.com/django-crispy-forms/crispy-bootstrap5
+django==5.1.5  # https://www.djangoproject.com/
+django-environ==0.12.0  # https://github.com/joke2k/django-environ
+django-model-utils==5.0.0  # https://github.com/jazzband/django-model-utils
+django-allauth==65.3.1  # https://github.com/pennersr/django-allauth
+django-crispy-forms==2.3  # https://github.com/django-crispy-forms/django-crispy-forms
+crispy-bootstrap5==2024.10 # https://github.com/django-crispy-forms/crispy-bootstrap5
 django-redis==5.4.0  # https://github.com/niwinz/django-redis
 asgiref==3.8.1 # https://github.com/django/asgiref
 
 # Django REST Framework
-djangorestframework==3.15.1  # https://github.com/encode/django-rest-framework
+djangorestframework==3.15.2  # https://github.com/encode/django-rest-framework
 coreapi==2.3.3  # https://github.com/core-api/python-client
 
 # django-ninja
-django-ninja==1.1.0  # https://github.com/vitalik/django-ninja
+django-ninja==1.3.0  # https://github.com/vitalik/django-ninja
 
-structlog==24.1.0
+structlog==25.1.0
 colorama==0.4.6
 django-ipware==7.0.1
 
-Werkzeug==3.0.3  # https://github.com/pallets/werkzeug
+Werkzeug==3.1.3  # https://github.com/pallets/werkzeug
 ipdb==0.13.13  # https://github.com/gotcha/ipdb
-psycopg2-binary==2.9.9 # https://github.com/psycopg/psycopg2
+psycopg[binary]==3.2.4 # https://github.com/psycopg/psycopg
 
 # Testing
 # ------------------------------------------------------------------------------
-pytest==8.2.1   # https://github.com/pytest-dev/pytest
+pytest==8.3.4   # https://github.com/pytest-dev/pytest
 pytest-sugar==1.0.0  # https://github.com/Frozenball/pytest-sugar
-pytest-cov==5.0.0
-pytest-asyncio==0.23.7 # https://github.com/pytest-dev/pytest-asyncio
+pytest-cov==6.0.0
+pytest-asyncio==0.25.2 # https://github.com/pytest-dev/pytest-asyncio
 pytest-mock==3.14.0 # https://github.com/pytest-dev/pytest-mock
 
 # Code quality
 # ------------------------------------------------------------------------------
 -r ruff.txt
-coverage==7.5.1  # https://github.com/nedbat/coveragepy
+-r coverage.txt
 -r black.txt
-pylint-django==2.5.5  # https://github.com/PyCQA/pylint-django
+pylint-django==2.6.1  # https://github.com/PyCQA/pylint-django
 pylint-celery==0.3  # https://github.com/PyCQA/pylint-celery
 
 # Django
 # ------------------------------------------------------------------------------
-factory-boy==3.3.0  # https://github.com/FactoryBoy/factory_boy
+factory-boy==3.3.1  # https://github.com/FactoryBoy/factory_boy
 
 django-extensions==3.2.3  # https://github.com/django-extensions/django-extensions
 django-coverage-plugin==3.1.0  # https://github.com/nedbat/django_coverage_plugin
-pytest-django==4.8.0  # https://github.com/pytest-dev/pytest-django
+pytest-django==4.9.0  # https://github.com/pytest-dev/pytest-django
 
 # pre-commit
 # ------------------------------------------------------------------------------
-pre-commit==3.7.1  # https://github.com/pre-commit/pre-commit
+pre-commit==4.1.0  # https://github.com/pre-commit/pre-commit
diff --git a/requirements/local.txt b/requirements/local.txt
index cc81fb0b545af28a4d2fb9fa429a379da5edc47f..69e860d803b85bd0437333f518064f5a59420c6f 100644
--- a/requirements/local.txt
+++ b/requirements/local.txt
@@ -1,9 +1,9 @@
 -r local-base.txt
 
-redis==5.0.4  # https://github.com/antirez/redis
+redis==5.2.1  # https://github.com/antirez/redis
 celery==5.4.0  # pyup: < 5.0  # https://github.com/celery/celery
-kombu==5.3.7
+kombu==5.4.2
 flower==2.0.1  # https://github.com/mher/flower
-uvicorn==0.29.0 # https://github.com/encode/uvicorn
-gunicorn==22.0.0 # https://github.com/benoitc/gunicorn
-amqp==5.2.0 # https://github.com/celery/py-amqp
+uvicorn==0.34.0 # https://github.com/encode/uvicorn
+gunicorn==23.0.0 # https://github.com/benoitc/gunicorn
+amqp==5.3.1 # https://github.com/celery/py-amqp
diff --git a/requirements/mypy.txt b/requirements/mypy.txt
new file mode 100644
index 0000000000000000000000000000000000000000..30ec0b6bdb860894261e1664320eff540a3ce81b
--- /dev/null
+++ b/requirements/mypy.txt
@@ -0,0 +1,3 @@
+mypy==1.14.1
+celery-types==0.22.0
+django-stubs[compatible-mypy]==5.1.2
diff --git a/requirements/ruff.txt b/requirements/ruff.txt
index 8c0d9cd9aca9a6bfc636acee371cbdb0bda4434a..c669d013c556aa38a63076e8bf15d06e9843a3b9 100644
--- a/requirements/ruff.txt
+++ b/requirements/ruff.txt
@@ -1 +1 @@
-ruff==0.4.4  # https://github.com/astral-sh/ruff
+ruff==0.9.3  # https://github.com/astral-sh/ruff
diff --git a/test_app/tests/celery/test_receivers.py b/test_app/tests/celery/test_receivers.py
index 4ed0228457fd19879c0c920012f765a06d1dd66f..16d97392ee1d57182d7d6d09633a771aa14a0d2d 100644
--- a/test_app/tests/celery/test_receivers.py
+++ b/test_app/tests/celery/test_receivers.py
@@ -1,32 +1,33 @@
 import logging
 from signal import SIGTERM
-from unittest.mock import Mock, patch, call, MagicMock
+from typing import Any, Optional, Type
+from unittest.mock import MagicMock, Mock, call, patch
 
 import structlog
 from celery import shared_task
 from django.contrib.auth.models import AnonymousUser
 from django.dispatch import receiver as django_receiver
-from django.test import TestCase, RequestFactory
+from django.test import RequestFactory, TestCase
 
 from django_structlog.celery import receivers, signals
 
 
 class TestReceivers(TestCase):
-    def setUp(self):
+    def setUp(self) -> None:
         self.factory = RequestFactory()
         self.logger = structlog.getLogger(__name__)
 
-    def tearDown(self):
+    def tearDown(self) -> None:
         structlog.contextvars.clear_contextvars()
 
-    def test_defer_task(self):
+    def test_defer_task(self) -> None:
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
         request = self.factory.get("/foo")
         request.user = AnonymousUser()
 
         @shared_task
-        def test_task(value):  # pragma: no cover
+        def test_task(value: Any) -> None:  # pragma: no cover
             pass
 
         receiver = receivers.CeleryReceiver()
@@ -37,18 +38,18 @@ class TestReceivers(TestCase):
         ) as log_results:
             test_task.delay("foo")
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_enqueued", record.msg["event"])
         self.assertEqual("INFO", record.levelname)
         self.assertIn("child_task_id", record.msg)
         self.assertEqual(expected_uuid, record.msg["request_id"])
 
-    def test_receiver_before_task_publish_celery_protocol_v2(self):
+    def test_receiver_before_task_publish_celery_protocol_v2(self) -> None:
         expected_uuid = "00000000-0000-0000-0000-000000000000"
         expected_user_id = "1234"
         expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111"
 
-        headers = {}
+        headers: dict[str, Any] = {}
         structlog.contextvars.bind_contextvars(
             request_id=expected_uuid,
             user_id=expected_user_id,
@@ -68,13 +69,13 @@ class TestReceivers(TestCase):
             headers,
         )
 
-    def test_receiver_before_task_publish_celery_protocol_v1(self):
+    def test_receiver_before_task_publish_celery_protocol_v1(self) -> None:
         """Protocol v1 does not allow to store metadata"""
         expected_uuid = "00000000-0000-0000-0000-000000000000"
         expected_user_id = "1234"
         expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111"
 
-        headers = {}
+        headers: dict[str, Any] = {}
         structlog.contextvars.bind_contextvars(
             request_id=expected_uuid,
             user_id=expected_user_id,
@@ -93,20 +94,27 @@ class TestReceivers(TestCase):
             headers,
         )
 
-    def test_signal_modify_context_before_task_publish_celery_protocol_v2(self):
+    def test_signal_modify_context_before_task_publish_celery_protocol_v2(self) -> None:
         expected_uuid = "00000000-0000-0000-0000-000000000000"
         user_id = "1234"
         expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111"
         routing_key = "foo"
-        properties = {"correlation_id": "22222222-2222-2222-2222-222222222222"}
+        properties: dict[str, Optional[str]] = {
+            "correlation_id": "22222222-2222-2222-2222-222222222222"
+        }
 
-        received_properties = None
-        received_routing_key = None
+        received_properties: Any = None
+        received_routing_key: Any = None
 
         @django_receiver(signals.modify_context_before_task_publish)
         def receiver_modify_context_before_task_publish(
-            sender, signal, context, task_properties, task_routing_key, **kwargs
-        ):
+            sender: Type[Any],
+            signal: Any,
+            context: Any,
+            task_properties: Any,
+            task_routing_key: str,
+            **kwargs: Any,
+        ) -> None:
             keys_to_keep = {"request_id", "parent_task_id"}
             new_dict = {
                 key_to_keep: context[key_to_keep]
@@ -120,7 +128,7 @@ class TestReceivers(TestCase):
             nonlocal received_routing_key
             received_routing_key = task_routing_key
 
-        headers = {}
+        headers: dict[str, Any] = {}
         structlog.contextvars.bind_contextvars(
             request_id=expected_uuid,
             user_id=user_id,
@@ -149,10 +157,10 @@ class TestReceivers(TestCase):
         )
         self.assertEqual("foo", received_routing_key)
 
-    def test_receiver_after_task_publish(self):
+    def test_receiver_after_task_publish(self) -> None:
         expected_task_id = "00000000-0000-0000-0000-000000000000"
         expected_task_name = "Foo"
-        headers = {"id": expected_task_id, "task": expected_task_name}
+        headers: dict[str, Any] = {"id": expected_task_id, "task": expected_task_name}
         receiver = receivers.CeleryReceiver()
         with self.assertLogs(
             logging.getLogger("django_structlog.celery.receivers"), logging.INFO
@@ -160,7 +168,7 @@ class TestReceivers(TestCase):
             receiver.receiver_after_task_publish(headers=headers)
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_enqueued", record.msg["event"])
         self.assertEqual("INFO", record.levelname)
         self.assertIn("child_task_id", record.msg)
@@ -168,10 +176,10 @@ class TestReceivers(TestCase):
         self.assertIn("child_task_name", record.msg)
         self.assertEqual(expected_task_name, record.msg["child_task_name"])
 
-    def test_receiver_after_task_publish_protocol_v1(self):
+    def test_receiver_after_task_publish_protocol_v1(self) -> None:
         expected_task_id = "00000000-0000-0000-0000-000000000000"
         expected_task_name = "Foo"
-        body = {"id": expected_task_id, "task": expected_task_name}
+        body: dict[str, Any] = {"id": expected_task_id, "task": expected_task_name}
 
         receiver = receivers.CeleryReceiver()
         with self.assertLogs(
@@ -180,7 +188,7 @@ class TestReceivers(TestCase):
             receiver.receiver_after_task_publish(body=body)
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_enqueued", record.msg["event"])
         self.assertEqual("INFO", record.levelname)
         self.assertIn("child_task_id", record.msg)
@@ -188,7 +196,7 @@ class TestReceivers(TestCase):
         self.assertIn("child_task_name", record.msg)
         self.assertEqual(expected_task_name, record.msg["child_task_name"])
 
-    def test_receiver_task_pre_run(self):
+    def test_receiver_task_pre_run(self) -> None:
         expected_request_uuid = "00000000-0000-0000-0000-000000000000"
         task_id = "11111111-1111-1111-1111-111111111111"
         expected_user_id = "1234"
@@ -220,17 +228,17 @@ class TestReceivers(TestCase):
         )
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_started", record.msg["event"])
         self.assertEqual("INFO", record.levelname)
         self.assertIn("task", record.msg)
         self.assertEqual("task_name", record.msg["task"])
 
-    def test_signal_bind_extra_task_metadata(self):
+    def test_signal_bind_extra_task_metadata(self) -> None:
         @django_receiver(signals.bind_extra_task_metadata)
         def receiver_bind_extra_request_metadata(
-            sender, signal, task=None, logger=None
-        ):
+            sender: Type[Any], signal: Any, task: Any = None, logger: Any = None
+        ) -> None:
             structlog.contextvars.bind_contextvars(
                 correlation_id=task.request.correlation_id
             )
@@ -251,7 +259,7 @@ class TestReceivers(TestCase):
         self.assertEqual(context["correlation_id"], expected_correlation_uuid)
         self.assertEqual(context["task_id"], task_id)
 
-    def test_receiver_task_retry(self):
+    def test_receiver_task_retry(self) -> None:
         expected_reason = "foo"
 
         receiver = receivers.CeleryReceiver()
@@ -261,19 +269,23 @@ class TestReceivers(TestCase):
             receiver.receiver_task_retry(reason=expected_reason)
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_retrying", record.msg["event"])
         self.assertEqual("WARNING", record.levelname)
         self.assertIn("reason", record.msg)
         self.assertEqual(expected_reason, record.msg["reason"])
 
-    def test_receiver_task_success(self):
+    def test_receiver_task_success(self) -> None:
         expected_result = "foo"
 
         @django_receiver(signals.pre_task_succeeded)
         def receiver_pre_task_succeeded(
-            sender, signal, task=None, logger=None, result=None
-        ):
+            sender: Type[Any],
+            signal: Any,
+            task: Any = None,
+            logger: Any = None,
+            result: Any = None,
+        ) -> None:
             structlog.contextvars.bind_contextvars(result=result)
 
         receiver = receivers.CeleryReceiver()
@@ -283,13 +295,13 @@ class TestReceivers(TestCase):
             receiver.receiver_task_success(result=expected_result)
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_succeeded", record.msg["event"])
         self.assertEqual("INFO", record.levelname)
         self.assertIn("result", record.msg)
         self.assertEqual(expected_result, record.msg["result"])
 
-    def test_receiver_task_failure(self):
+    def test_receiver_task_failure(self) -> None:
         expected_exception = "foo"
         receiver = receivers.CeleryReceiver()
         with self.assertLogs(
@@ -298,14 +310,14 @@ class TestReceivers(TestCase):
             receiver.receiver_task_failure(exception=Exception("foo"))
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_failed", record.msg["event"])
         self.assertEqual("ERROR", record.levelname)
         self.assertIn("error", record.msg)
         self.assertIn("exception", record.msg)
         self.assertEqual(expected_exception, record.msg["error"])
 
-    def test_receiver_task_failure_with_throws(self):
+    def test_receiver_task_failure_with_throws(self) -> None:
         expected_exception = "foo"
 
         mock_sender = Mock()
@@ -319,14 +331,14 @@ class TestReceivers(TestCase):
             )
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_failed", record.msg["event"])
         self.assertEqual("INFO", record.levelname)
         self.assertIn("error", record.msg)
         self.assertNotIn("exception", record.msg)
         self.assertEqual(expected_exception, record.msg["error"])
 
-    def test_receiver_task_revoked(self):
+    def test_receiver_task_revoked(self) -> None:
         expected_request_uuid = "00000000-0000-0000-0000-000000000000"
         task_id = "11111111-1111-1111-1111-111111111111"
         expected_user_id = "1234"
@@ -348,7 +360,7 @@ class TestReceivers(TestCase):
             )
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_revoked", record.msg["event"])
         self.assertEqual("WARNING", record.levelname)
         self.assertIn("terminated", record.msg)
@@ -368,7 +380,7 @@ class TestReceivers(TestCase):
         self.assertIn("user_id", record.msg)
         self.assertEqual(expected_user_id, record.msg["user_id"])
 
-    def test_receiver_task_revoked_terminated(self):
+    def test_receiver_task_revoked_terminated(self) -> None:
         expected_request_uuid = "00000000-0000-0000-0000-000000000000"
         task_id = "11111111-1111-1111-1111-111111111111"
         expected_user_id = "1234"
@@ -390,7 +402,7 @@ class TestReceivers(TestCase):
             )
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_revoked", record.msg["event"])
         self.assertEqual("WARNING", record.levelname)
         self.assertIn("terminated", record.msg)
@@ -410,7 +422,7 @@ class TestReceivers(TestCase):
         self.assertIn("user_id", record.msg)
         self.assertEqual(expected_user_id, record.msg["user_id"])
 
-    def test_receiver_task_unknown(self):
+    def test_receiver_task_unknown(self) -> None:
         task_id = "11111111-1111-1111-1111-111111111111"
         expected_task_name = "task_name"
 
@@ -421,7 +433,7 @@ class TestReceivers(TestCase):
             receiver.receiver_task_unknown(id=task_id, name=expected_task_name)
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_not_found", record.msg["event"])
         self.assertEqual("ERROR", record.levelname)
         self.assertIn("task_id", record.msg)
@@ -429,7 +441,7 @@ class TestReceivers(TestCase):
         self.assertIn("task", record.msg)
         self.assertEqual(expected_task_name, record.msg["task"])
 
-    def test_receiver_task_rejected(self):
+    def test_receiver_task_rejected(self) -> None:
         task_id = "11111111-1111-1111-1111-111111111111"
         message = Mock(name="message")
         message.properties = dict(correlation_id=task_id)
@@ -441,13 +453,13 @@ class TestReceivers(TestCase):
             receiver.receiver_task_rejected(message=message)
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_rejected", record.msg["event"])
         self.assertEqual("ERROR", record.levelname)
         self.assertIn("task_id", record.msg)
         self.assertEqual(task_id, record.msg["task_id"])
 
-    def test_priority(self):
+    def test_priority(self) -> None:
         expected_uuid = "00000000-0000-0000-0000-000000000000"
         user_id = "1234"
         expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111"
@@ -455,7 +467,7 @@ class TestReceivers(TestCase):
         expected_priority = 6
         properties = {"priority": expected_priority}
 
-        headers = {}
+        headers: dict[str, Any] = {}
         structlog.contextvars.bind_contextvars(
             request_id=expected_uuid,
             user_id=user_id,
@@ -492,7 +504,7 @@ class TestReceivers(TestCase):
             )
 
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("task_enqueued", record.msg["event"])
         self.assertEqual("INFO", record.levelname)
         self.assertIn("child_task_id", record.msg)
@@ -508,17 +520,17 @@ class TestReceivers(TestCase):
 
 
 class TestConnectCeleryTaskSignals(TestCase):
-    def test_call(self):
+    def test_call(self) -> None:
         from celery.signals import (
-            before_task_publish,
             after_task_publish,
+            before_task_publish,
+            task_failure,
             task_prerun,
+            task_rejected,
             task_retry,
-            task_success,
-            task_failure,
             task_revoked,
+            task_success,
             task_unknown,
-            task_rejected,
         )
 
         from django_structlog.celery.receivers import CeleryReceiver
@@ -545,8 +557,9 @@ class TestConnectCeleryTaskSignals(TestCase):
 
 
 class TestConnectCelerySignals(TestCase):
-    def test_call(self):
-        from celery.signals import before_task_publish, after_task_publish
+    def test_call(self) -> None:
+        from celery.signals import after_task_publish, before_task_publish
+
         from django_structlog.celery.receivers import CeleryReceiver
 
         receiver = CeleryReceiver()
diff --git a/test_app/tests/celery/test_steps.py b/test_app/tests/celery/test_steps.py
index d323987a76dacd030cbf96abda157a21a9ba4aa5..58b687e46183c33c2790d302780eb8f0527514ed 100644
--- a/test_app/tests/celery/test_steps.py
+++ b/test_app/tests/celery/test_steps.py
@@ -6,7 +6,7 @@ from django_structlog.celery import steps
 
 
 class TestDjangoStructLogInitStep(TestCase):
-    def test_call(self):
+    def test_call(self) -> None:
         with patch(
             "django_structlog.celery.receivers.CeleryReceiver.connect_worker_signals",
             autospec=True,
diff --git a/test_app/tests/middlewares/test_request.py b/test_app/tests/middlewares/test_request.py
index f3255e2a9cba9c020598f2e17a5baf44059c56c8..d8088b6874cd96723c49b5cfc5de4d47653b30a8 100644
--- a/test_app/tests/middlewares/test_request.py
+++ b/test_app/tests/middlewares/test_request.py
@@ -2,39 +2,45 @@ import asyncio
 import logging
 import traceback
 import uuid
+from typing import Any, AsyncGenerator, Awaitable, Generator, Type, cast
 from unittest import mock
-from unittest.mock import patch, Mock, AsyncMock
+from unittest.mock import AsyncMock, Mock, patch
 
+import structlog
 from django.contrib.auth.models import AnonymousUser, User
 from django.contrib.sites.models import Site
 from django.contrib.sites.shortcuts import get_current_site
 from django.core.exceptions import PermissionDenied
+from django.core.signals import got_request_exception
 from django.dispatch import receiver
 from django.http import (
     Http404,
-    HttpResponseNotFound,
+    HttpRequest,
+    HttpResponse,
     HttpResponseForbidden,
+    HttpResponseNotFound,
     HttpResponseServerError,
     StreamingHttpResponse,
 )
-from django.test import TestCase, RequestFactory, override_settings
-import structlog
+from django.test import RequestFactory, TestCase, override_settings
 
-from django_structlog import middlewares
 from django_structlog.middlewares.request import (
+    RequestMiddleware,
+    async_streaming_content_wrapper,
     get_request_header,
     sync_streaming_content_wrapper,
-    async_streaming_content_wrapper,
 )
 from django_structlog.signals import (
-    bind_extra_request_metadata,
-    bind_extra_request_finished_metadata,
     bind_extra_request_failed_metadata,
+    bind_extra_request_finished_metadata,
+    bind_extra_request_metadata,
 )
 
 
 class TestRequestMiddleware(TestCase):
-    def setUp(self):
+    log_results: Any
+
+    def setUp(self) -> None:
         self.factory = RequestFactory()
         self.logger = structlog.getLogger(__name__)
         self.log_results = None
@@ -43,15 +49,15 @@ class TestRequestMiddleware(TestCase):
             defaults={"domain": "testserver", "name": "django_structlog_demo_project"},
         )
 
-    def tearDown(self):
+    def tearDown(self) -> None:
         structlog.contextvars.clear_contextvars()
 
-    def test_process_request_without_user(self):
+    def test_process_request_without_user(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             with self.assertLogs(__name__, logging.INFO) as log_results:
                 self.logger.info("hello")
             self.log_results = log_results
@@ -59,7 +65,7 @@ class TestRequestMiddleware(TestCase):
 
         request = self.factory.get("/foo")
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
         with patch("uuid.UUID.__str__", return_value=expected_uuid):
             middleware(request)
 
@@ -76,21 +82,21 @@ class TestRequestMiddleware(TestCase):
         record = log_results.records[0]
         self.assertNotIn("request_id", record.msg)
 
-    def test_process_request_with_null_user(self):
+    def test_process_request_with_null_user(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             with self.assertLogs(__name__, logging.INFO) as log_results:
                 self.logger.info("hello")
             self.log_results = log_results
             return mock_response
 
         request = self.factory.get("/foo")
-        request.user = None
+        setattr(request, "user", None)
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
         with patch("uuid.UUID.__str__", return_value=expected_uuid):
             middleware(request)
 
@@ -107,12 +113,12 @@ class TestRequestMiddleware(TestCase):
         record = log_results.records[0]
         self.assertNotIn("request_id", record.msg)
 
-    def test_process_request_anonymous(self):
+    def test_process_request_anonymous(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             with self.assertLogs(__name__, logging.INFO) as log_results:
                 self.logger.info("hello")
             self.log_results = log_results
@@ -121,7 +127,7 @@ class TestRequestMiddleware(TestCase):
         request = self.factory.get("/foo")
         request.user = AnonymousUser()
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
         with patch("uuid.UUID.__str__", return_value=expected_uuid):
             middleware(request)
 
@@ -140,12 +146,12 @@ class TestRequestMiddleware(TestCase):
         self.assertNotIn("request_id", record.msg)
         self.assertNotIn("user_id", record.msg)
 
-    def test_process_request_user(self):
+    def test_process_request_user(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             with self.assertLogs(__name__, logging.INFO) as log_results:
                 self.logger.info("hello")
             self.log_results = log_results
@@ -153,10 +159,10 @@ class TestRequestMiddleware(TestCase):
 
         request = self.factory.get("/foo")
 
-        mock_user = User.objects.create()
+        mock_user: Any = User.objects.create()
         request.user = mock_user
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
         with patch("uuid.UUID.__str__", return_value=expected_uuid):
             middleware(request)
 
@@ -176,12 +182,12 @@ class TestRequestMiddleware(TestCase):
         self.assertNotIn("request_id", record.msg)
         self.assertNotIn("user_id", record.msg)
 
-    def test_process_request_user_uuid(self):
+    def test_process_request_user_uuid(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             with self.assertLogs(__name__, logging.INFO) as log_results:
                 self.logger.info("hello")
             self.log_results = log_results
@@ -189,11 +195,11 @@ class TestRequestMiddleware(TestCase):
 
         request = self.factory.get("/foo")
 
-        mock_user = mock.Mock()
+        mock_user: Any = mock.Mock()
         mock_user.pk = uuid.UUID(expected_uuid)
         request.user = mock_user
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
         middleware(request)
 
         self.assertEqual(1, len(self.log_results.records))
@@ -204,11 +210,11 @@ class TestRequestMiddleware(TestCase):
         self.assertIsInstance(record.msg["user_id"], str)
         self.assertEqual(expected_uuid, record.msg["user_id"])
 
-    def test_process_request_user_without_id(self):
+    def test_process_request_user_without_id(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             with self.assertLogs(__name__, logging.INFO) as log_results:
                 self.logger.info("hello")
             self.log_results = log_results
@@ -219,8 +225,8 @@ class TestRequestMiddleware(TestCase):
         class SimpleUser:
             pass
 
-        request.user = SimpleUser()
-        middleware = middlewares.RequestMiddleware(get_response)
+        request.user = cast(Any, SimpleUser())
+        middleware = RequestMiddleware(get_response)
         middleware(request)
 
         self.assertEqual(1, len(self.log_results.records))
@@ -230,26 +236,30 @@ class TestRequestMiddleware(TestCase):
         self.assertIn("user_id", record.msg)
         self.assertIsNone(record.msg["user_id"])
 
-    def test_log_user_in_request_finished(self):
+    def test_log_user_in_request_finished(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
-        mock_user = User.objects.create()
+        mock_user: Any = User.objects.create()
 
         request = self.factory.get("/foo")
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             request.user = mock_user
             return mock_response
 
-        middleware = middlewares.RequestMiddleware(get_response)
-        with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs(
-            "django_structlog.middlewares.request", logging.INFO
-        ) as log_results:
+        middleware = RequestMiddleware(get_response)
+        with (
+            patch("uuid.UUID.__str__", return_value=expected_uuid),
+            self.assertLogs(
+                "django_structlog.middlewares.request", logging.INFO
+            ) as log_results,
+        ):
             middleware(request)
 
         self.assertEqual(2, len(log_results.records))
+        record: Any
         record = log_results.records[0]
 
         self.assertEqual("INFO", record.levelname)
@@ -267,35 +277,39 @@ class TestRequestMiddleware(TestCase):
         self.assertIn("user_id", record.msg)
         self.assertEqual(mock_user.id, record.msg["user_id"])
 
-    def test_log_user_in_request_finished_with_exception(self):
+    def test_log_user_in_request_finished_with_exception(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
-        mock_user = User.objects.create()
+        mock_user: Any = User.objects.create()
 
         request = self.factory.get("/foo")
 
-        middleware = middlewares.RequestMiddleware(None)
-        exception = Exception()
-
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             request.user = mock_user
             try:
                 raise exception
-            except Exception as e:
-                middleware.process_exception(request, e)
+            except Exception:
+                got_request_exception.send(object, request=request)
                 self.exception_traceback = traceback.format_exc()
             return mock_response
 
+        middleware = RequestMiddleware(get_response)
+        exception = Exception()
+
         middleware.get_response = get_response
 
-        with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs(
-            "django_structlog.middlewares.request", logging.INFO
-        ) as log_results:
+        with (
+            patch("uuid.UUID.__str__", return_value=expected_uuid),
+            self.assertLogs(
+                "django_structlog.middlewares.request", logging.INFO
+            ) as log_results,
+        ):
             middleware(request)
 
         self.assertEqual(2, len(log_results.records))
+        record: Any
         record = log_results.records[0]
 
         self.assertEqual("INFO", record.levelname)
@@ -315,16 +329,16 @@ class TestRequestMiddleware(TestCase):
         self.assertIn("user_id", record.msg)
         self.assertEqual(mock_user.id, record.msg["user_id"])
 
-    def test_signal_bind_extra_request_metadata(self):
+    def test_signal_bind_extra_request_metadata(self) -> None:
         @receiver(bind_extra_request_metadata)
         def receiver_bind_extra_request_metadata(
-            sender,
-            signal,
-            request=None,
-            logger=None,
-            log_kwargs=None,
-            **kwargs,
-        ):
+            sender: Type[Any],
+            signal: Any,
+            request: Any = None,
+            logger: Any = None,
+            log_kwargs: Any = None,
+            **kwargs: Any,
+        ) -> None:
             current_site = get_current_site(request)
             log_kwargs["request_started_log"] = "foo"
             structlog.contextvars.bind_contextvars(domain=current_site.domain)
@@ -332,7 +346,7 @@ class TestRequestMiddleware(TestCase):
         mock_response = Mock()
         mock_response.status_code = 200
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             with self.assertLogs(__name__, logging.INFO) as log_results:
                 self.logger.info("hello")
             self.log_results = log_results
@@ -340,10 +354,10 @@ class TestRequestMiddleware(TestCase):
 
         request = self.factory.get("/foo")
 
-        mock_user = User.objects.create(email="foo@example.com")
+        mock_user: Any = User.objects.create(email="foo@example.com")
         request.user = mock_user
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
 
         with self.assertLogs(
             "django_structlog.middlewares.request", logging.INFO
@@ -368,28 +382,33 @@ class TestRequestMiddleware(TestCase):
         self.assertEqual("request_finished", record.msg["event"])
         self.assertNotIn("request_started_log", record.msg)
 
-    def test_signal_bind_extra_request_finished_metadata(self):
+    def test_signal_bind_extra_request_finished_metadata(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
 
         @receiver(bind_extra_request_finished_metadata)
         def receiver_bind_extra_request_finished_metadata(
-            sender, signal, request=None, logger=None, response=None, log_kwargs=None
-        ):
+            sender: Type[Any],
+            signal: Any,
+            request: Any = None,
+            logger: Any = None,
+            response: Any = None,
+            log_kwargs: Any = None,
+        ) -> None:
             self.assertEqual(response, mock_response)
             current_site = get_current_site(request)
             log_kwargs["request_finished_log"] = "foo"
             structlog.contextvars.bind_contextvars(domain=current_site.domain)
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             return mock_response
 
         request = self.factory.get("/foo")
 
-        mock_user = User.objects.create(email="foo@example.com")
+        mock_user: Any = User.objects.create(email="foo@example.com")
         request.user = mock_user
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
 
         with self.assertLogs(
             "django_structlog.middlewares.request", logging.INFO
@@ -397,6 +416,7 @@ class TestRequestMiddleware(TestCase):
             middleware(request)
 
         self.assertEqual(2, len(log_results.records))
+        record: Any
         record = log_results.records[0]
 
         self.assertEqual("INFO", record.levelname)
@@ -419,19 +439,19 @@ class TestRequestMiddleware(TestCase):
         self.assertIn("request_finished_log", record.msg)
         self.assertEqual("foo", record.msg["request_finished_log"])
 
-    def test_signal_bind_extra_request_failed_metadata(self):
+    def test_signal_bind_extra_request_failed_metadata(self) -> None:
         expected_exception = Exception()
 
         @receiver(bind_extra_request_failed_metadata)
         def receiver_bind_extra_request_failed_metadata(
-            sender,
-            signal,
-            request=None,
-            response=None,
-            logger=None,
-            exception=None,
-            log_kwargs=None,
-        ):
+            sender: Type[Any],
+            signal: Any,
+            request: Any = None,
+            response: Any = None,
+            logger: Any = None,
+            exception: Any = None,
+            log_kwargs: Any = None,
+        ) -> None:
             self.assertEqual(exception, expected_exception)
             current_site = get_current_site(request)
             log_kwargs["request_failed_log"] = "foo"
@@ -439,18 +459,20 @@ class TestRequestMiddleware(TestCase):
 
         request = self.factory.get("/foo")
 
-        mock_user = User.objects.create(email="foo@example.com")
+        mock_user: Any = User.objects.create(email="foo@example.com")
 
         request.user = mock_user
-        middleware = middlewares.RequestMiddleware(None)
 
-        mock_response = Mock()
-
-        def get_response(_response):
-            middleware.process_exception(request, expected_exception)
+        def get_response(_request: HttpRequest) -> HttpResponse:
+            try:
+                raise expected_exception
+            except Exception:
+                got_request_exception.send(object, request=request)
             return mock_response
 
-        middleware.get_response = get_response
+        middleware = RequestMiddleware(get_response)
+
+        mock_response = Mock()
 
         with self.assertLogs(
             "django_structlog.middlewares.request", logging.INFO
@@ -458,6 +480,8 @@ class TestRequestMiddleware(TestCase):
             middleware(request)
 
         self.assertEqual(2, len(log_results.records))
+
+        record: Any
         record = log_results.records[0]
 
         self.assertEqual("INFO", record.levelname)
@@ -480,32 +504,34 @@ class TestRequestMiddleware(TestCase):
         self.assertIn("request_failed_log", record.msg)
         self.assertEqual("foo", record.msg["request_failed_log"])
 
-    def test_process_request_error(self):
+    def test_process_request_error(self) -> None:
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
         request = self.factory.get("/foo")
         request.user = AnonymousUser()
 
-        middleware = middlewares.RequestMiddleware(None)
-
-        exception = Exception("This is an exception")
-
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> Any:
             """Simulate an exception"""
             try:
                 raise exception
-            except Exception as e:
-                middleware.process_exception(request, e)
+            except Exception:
+                got_request_exception.send(object, request=request)
                 self.exception_traceback = traceback.format_exc()
 
-        middleware.get_response = get_response
+        middleware = RequestMiddleware(get_response)
 
-        with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs(
-            logging.getLogger("django_structlog"), logging.INFO
-        ) as log_results:
+        exception = Exception("This is an exception")
+
+        with (
+            patch("uuid.UUID.__str__", return_value=expected_uuid),
+            self.assertLogs(
+                logging.getLogger("django_structlog"), logging.INFO
+            ) as log_results,
+        ):
             middleware(request)
 
         self.assertEqual(2, len(log_results.records))
+        record: Any
         record = log_results.records[0]
         self.assertEqual("INFO", record.levelname)
         self.assertIn("request_id", record.msg)
@@ -534,29 +560,33 @@ class TestRequestMiddleware(TestCase):
         self.assertNotIn("user_id", record.msg)
         self.assertFalse(hasattr(request, "_raised_exception"))
 
-    def test_process_request_403_are_processed_as_regular_requests(self):
+    def test_process_request_403_are_processed_as_regular_requests(self) -> None:
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
         request = self.factory.get("/foo")
         request.user = AnonymousUser()
 
-        middleware = middlewares.RequestMiddleware(None)
-
-        exception = PermissionDenied()
-
-        def get_response(_response):
-            """Simulate an exception"""
-            middleware.process_exception(request, exception)
+        def get_response(_request: HttpRequest) -> HttpResponse:
+            try:
+                raise exception
+            except Exception:
+                got_request_exception.send(object, request=request)
             return HttpResponseForbidden()
 
-        middleware.get_response = get_response
+        middleware = RequestMiddleware(get_response)
 
-        with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs(
-            logging.getLogger("django_structlog"), logging.INFO
-        ) as log_results:
+        exception = PermissionDenied()
+
+        with (
+            patch("uuid.UUID.__str__", return_value=expected_uuid),
+            self.assertLogs(
+                logging.getLogger("django_structlog"), logging.INFO
+            ) as log_results,
+        ):
             middleware(request)
 
         self.assertEqual(2, len(log_results.records))
+        record: Any
         record = log_results.records[0]
         self.assertEqual("INFO", record.levelname)
         self.assertIn("request_id", record.msg)
@@ -584,29 +614,33 @@ class TestRequestMiddleware(TestCase):
         self.assertNotIn("user_id", record.msg)
         self.assertFalse(hasattr(request, "_raised_exception"))
 
-    def test_process_request_404_are_processed_as_regular_requests(self):
+    def test_process_request_404_are_processed_as_regular_requests(self) -> None:
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
         request = self.factory.get("/foo")
         request.user = AnonymousUser()
 
-        middleware = middlewares.RequestMiddleware(None)
-
-        exception = Http404()
-
-        def get_response(_response):
-            """Simulate an exception"""
-            middleware.process_exception(request, exception)
+        def get_response(_request: HttpRequest) -> HttpResponse:
+            try:
+                raise exception
+            except Exception:
+                got_request_exception.send(object, request=request)
             return HttpResponseNotFound()
 
-        middleware.get_response = get_response
+        middleware = RequestMiddleware(get_response)
 
-        with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs(
-            logging.getLogger("django_structlog"), logging.INFO
-        ) as log_results:
+        exception = Http404()
+
+        with (
+            patch("uuid.UUID.__str__", return_value=expected_uuid),
+            self.assertLogs(
+                logging.getLogger("django_structlog"), logging.INFO
+            ) as log_results,
+        ):
             middleware(request)
 
         self.assertEqual(2, len(log_results.records))
+        record: Any
         record = log_results.records[0]
         self.assertEqual("INFO", record.levelname)
         self.assertIn("request_id", record.msg)
@@ -635,29 +669,33 @@ class TestRequestMiddleware(TestCase):
         self.assertFalse(hasattr(request, "_raised_exception"))
 
     @override_settings(DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL=logging.INFO)
-    def test_process_request_4XX_can_be_personalized(self):
+    def test_process_request_4XX_can_be_personalized(self) -> None:
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
         request = self.factory.get("/foo")
         request.user = AnonymousUser()
 
-        middleware = middlewares.RequestMiddleware(None)
-
-        exception = Http404()
-
-        def get_response(_response):
-            """Simulate an exception"""
-            middleware.process_exception(request, exception)
+        def get_response(_request: HttpRequest) -> HttpResponse:
+            try:
+                raise exception
+            except Exception:
+                got_request_exception.send(object, request=request)
             return HttpResponseNotFound()
 
-        middleware.get_response = get_response
+        middleware = RequestMiddleware(get_response)
 
-        with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs(
-            logging.getLogger("django_structlog"), logging.INFO
-        ) as log_results:
+        exception = Http404()
+
+        with (
+            patch("uuid.UUID.__str__", return_value=expected_uuid),
+            self.assertLogs(
+                logging.getLogger("django_structlog"), logging.INFO
+            ) as log_results,
+        ):
             middleware(request)
 
         self.assertEqual(2, len(log_results.records))
+        record: Any
         record = log_results.records[0]
         self.assertEqual("INFO", record.levelname)
         self.assertIn("request_id", record.msg)
@@ -685,25 +723,27 @@ class TestRequestMiddleware(TestCase):
         self.assertNotIn("user_id", record.msg)
         self.assertFalse(hasattr(request, "_raised_exception"))
 
-    def test_process_request_500_are_processed_as_errors(self):
+    def test_process_request_500_are_processed_as_errors(self) -> None:
         expected_uuid = "00000000-0000-0000-0000-000000000000"
 
         request = self.factory.get("/foo")
         request.user = AnonymousUser()
 
-        middleware = middlewares.RequestMiddleware(None)
-
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             return HttpResponseServerError()
 
-        middleware.get_response = get_response
+        middleware = RequestMiddleware(get_response)
 
-        with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs(
-            logging.getLogger("django_structlog"), logging.INFO
-        ) as log_results:
+        with (
+            patch("uuid.UUID.__str__", return_value=expected_uuid),
+            self.assertLogs(
+                logging.getLogger("django_structlog"), logging.INFO
+            ) as log_results,
+        ):
             middleware(request)
 
         self.assertEqual(2, len(log_results.records))
+        record: Any
         record = log_results.records[0]
         self.assertEqual("INFO", record.levelname)
         self.assertIn("request_id", record.msg)
@@ -731,12 +771,12 @@ class TestRequestMiddleware(TestCase):
         self.assertNotIn("user_id", record.msg)
         self.assertFalse(hasattr(request, "_raised_exception"))
 
-    def test_should_log_request_id_from_request_x_request_id_header(self):
+    def test_should_log_request_id_from_request_x_request_id_header(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
         x_request_id = "my-fake-request-id"
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             with self.assertLogs(__name__, logging.INFO) as log_results:
                 self.logger.info("hello")
             self.log_results = log_results
@@ -744,10 +784,11 @@ class TestRequestMiddleware(TestCase):
 
         request = RequestFactory(HTTP_X_REQUEST_ID=x_request_id).get("/foo")
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
         middleware(request)
 
         self.assertEqual(1, len(self.log_results.records))
+        record: Any
         record = self.log_results.records[0]
 
         self.assertEqual("INFO", record.levelname)
@@ -755,12 +796,14 @@ class TestRequestMiddleware(TestCase):
         self.assertNotIn("user_id", record.msg)
         self.assertEqual(x_request_id, record.msg["request_id"])
 
-    def test_should_log_correlation_id_from_request_x_correlation_id_header(self):
+    def test_should_log_correlation_id_from_request_x_correlation_id_header(
+        self,
+    ) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
         x_correlation_id = "my-fake-correlation-id"
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> HttpResponse:
             with self.assertLogs(__name__, logging.INFO) as log_results:
                 self.logger.info("hello")
             self.log_results = log_results
@@ -768,10 +811,11 @@ class TestRequestMiddleware(TestCase):
 
         request = RequestFactory(HTTP_X_CORRELATION_ID=x_correlation_id).get("/foo")
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
         middleware(request)
 
         self.assertEqual(1, len(self.log_results.records))
+        record: Any
         record = self.log_results.records[0]
 
         self.assertEqual("INFO", record.levelname)
@@ -779,113 +823,125 @@ class TestRequestMiddleware(TestCase):
         self.assertNotIn("user_id", record.msg)
         self.assertEqual(x_correlation_id, record.msg["correlation_id"])
 
-    def test_sync_streaming_response(self):
-        def streaming_content():
+    def test_sync_streaming_response(self) -> None:
+        def streaming_content() -> Generator[Any, None, None]:  # pragma: no cover
             yield
 
         mock_response = mock.create_autospec(StreamingHttpResponse)
         mock_response.streaming_content = streaming_content()
+        mock_response.is_async = False
         mock_response.status_code = 200
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> Any:
             return mock_response
 
         request = RequestFactory().get("/foo")
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
 
         mock_wrapper = AsyncMock()
         with patch(
             "django_structlog.middlewares.request.sync_streaming_content_wrapper",
             return_value=mock_wrapper,
         ) as mock_sync_streaming_response_wrapper:
-            response = middleware(request)
+            response: Any = middleware(request)
 
         mock_sync_streaming_response_wrapper.assert_called_once()
         self.assertEqual(response.streaming_content, mock_wrapper)
 
-    def test_async_streaming_response(self):
-        async def streaming_content():
+    def test_async_streaming_response(self) -> None:
+        async def streaming_content() -> AsyncGenerator[Any, None]:  # pragma: no cover
             yield
 
         mock_response = mock.create_autospec(StreamingHttpResponse)
         mock_response.streaming_content = streaming_content()
+        mock_response.is_async = True
         mock_response.status_code = 200
 
-        def get_response(_response):
+        def get_response(_request: HttpRequest) -> Any:
             return mock_response
 
         request = RequestFactory().get("/foo")
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
 
         mock_wrapper = AsyncMock()
         with patch(
             "django_structlog.middlewares.request.async_streaming_content_wrapper",
             return_value=mock_wrapper,
         ) as mock_sync_streaming_response_wrapper:
-            response = middleware(request)
+            response: Any = middleware(request)
 
         mock_sync_streaming_response_wrapper.assert_called_once()
         self.assertEqual(response.streaming_content, mock_wrapper)
 
-    async def test_async_cancel(self):
-        async def async_get_response(request):
+    async def test_async_cancel(self) -> None:
+        async def async_get_response(request: HttpRequest) -> Any:
             raise asyncio.CancelledError
 
-        middleware = middlewares.RequestMiddleware(async_get_response)
+        middleware = RequestMiddleware(async_get_response)
 
         mock_request = Mock()
-        with patch(
-            "django_structlog.middlewares.request.RequestMiddleware.prepare"
-        ) as mock_prepare, patch(
-            "django_structlog.middlewares.request.RequestMiddleware.handle_response"
-        ) as mock_handle_response, self.assertLogs(
-            "django_structlog.middlewares.request", logging.WARNING
-        ) as log_results:
+        with (
+            patch(
+                "django_structlog.middlewares.RequestMiddleware.prepare"
+            ) as mock_prepare,
+            patch(
+                "django_structlog.middlewares.RequestMiddleware.handle_response"
+            ) as mock_handle_response,
+            self.assertLogs(
+                "django_structlog.middlewares.request", logging.WARNING
+            ) as log_results,
+        ):
             with self.assertRaises(asyncio.CancelledError):
-                await middleware(mock_request)
+                await cast(Awaitable[HttpResponse], middleware(mock_request))
         mock_prepare.assert_called_once_with(mock_request)
         mock_handle_response.assert_not_called()
         self.assertEqual(1, len(log_results.records))
-        record = log_results.records[0]
+        record: Any = log_results.records[0]
         self.assertEqual("request_cancelled", record.msg["event"])
 
 
 class TestRequestMiddlewareRouter(TestCase):
-    async def test_async(self):
+    async def test_async(self) -> None:
         mock_response = Mock()
 
-        async def async_get_response(request):
+        async def async_get_response(request: HttpRequest) -> Any:
             return mock_response
 
-        middleware = middlewares.RequestMiddleware(async_get_response)
+        middleware = RequestMiddleware(async_get_response)
 
         mock_request = Mock()
-        with patch(
-            "django_structlog.middlewares.request.RequestMiddleware.prepare"
-        ) as mock_prepare, patch(
-            "django_structlog.middlewares.request.RequestMiddleware.handle_response"
-        ) as mock_handle_response:
-            response = await middleware(mock_request)
+        with (
+            patch(
+                "django_structlog.middlewares.RequestMiddleware.prepare"
+            ) as mock_prepare,
+            patch(
+                "django_structlog.middlewares.RequestMiddleware.handle_response"
+            ) as mock_handle_response,
+        ):
+            response = await cast(Awaitable[HttpResponse], middleware(mock_request))
         self.assertEqual(response, mock_response)
         mock_prepare.assert_called_once_with(mock_request)
         mock_handle_response.assert_called_once_with(mock_request, mock_response)
 
-    def test_sync(self):
+    def test_sync(self) -> None:
         mock_response = Mock()
 
-        def get_response(request):
+        def get_response(request: HttpRequest) -> HttpResponse:
             return mock_response
 
-        middleware = middlewares.RequestMiddleware(get_response)
+        middleware = RequestMiddleware(get_response)
 
         mock_request = Mock()
-        with patch(
-            "django_structlog.middlewares.request.RequestMiddleware.prepare"
-        ) as mock_prepare, patch(
-            "django_structlog.middlewares.request.RequestMiddleware.handle_response"
-        ) as mock_handle_response:
+        with (
+            patch(
+                "django_structlog.middlewares.RequestMiddleware.prepare"
+            ) as mock_prepare,
+            patch(
+                "django_structlog.middlewares.RequestMiddleware.handle_response"
+            ) as mock_handle_response,
+        ):
             response = middleware(mock_request)
         self.assertEqual(response, mock_response)
         mock_prepare.assert_called_once_with(mock_request)
@@ -893,39 +949,41 @@ class TestRequestMiddlewareRouter(TestCase):
 
 
 class TestGetRequestHeader(TestCase):
-    def test_django_22_or_higher(self):
+    def test_django_22_or_higher(self) -> None:
         mock_request = mock.MagicMock(spec=["headers"])
         get_request_header(mock_request, "x-foo-bar", "HTTP_X_FOO_BAR")
         mock_request.headers.get.assert_called_once_with("x-foo-bar")
 
-    def test_django_prior_to_22(self):
+    def test_django_prior_to_22(self) -> None:
         mock_request = mock.MagicMock(spec=["META"])
         get_request_header(mock_request, "x-foo-bar", "HTTP_X_FOO_BAR")
         mock_request.META.get.assert_called_once_with("HTTP_X_FOO_BAR")
 
 
 class TestSyncStreamingContentWrapper(TestCase):
-    def setUp(self):
+    def setUp(self) -> None:
         self.logger = structlog.getLogger(__name__)
 
-    def test_success(self):
+    def test_success(self) -> None:
         result = Mock()
 
-        def streaming_content():
+        def streaming_content() -> Generator[Any, None, None]:
             self.logger.info("streaming_content")
             yield result
 
         wrapped_streaming_content = sync_streaming_content_wrapper(
             streaming_content(), {"foo": "bar"}
         )
-        with self.assertLogs(
-            __name__, logging.INFO
-        ) as streaming_content_log_results, self.assertLogs(
-            "django_structlog.middlewares.request", logging.INFO
-        ) as log_results:
+        with (
+            self.assertLogs(__name__, logging.INFO) as streaming_content_log_results,
+            self.assertLogs(
+                "django_structlog.middlewares.request", logging.INFO
+            ) as log_results,
+        ):
             self.assertEqual(result, next(wrapped_streaming_content))
 
         self.assertEqual(1, len(log_results.records))
+        record: Any
         record = log_results.records[0]
         self.assertEqual("INFO", record.levelname)
         self.assertEqual("streaming_started", record.msg["event"])
@@ -950,12 +1008,15 @@ class TestSyncStreamingContentWrapper(TestCase):
         self.assertIn("foo", record.msg)
         self.assertEqual("bar", record.msg["foo"])
 
-    def test_failure(self):
+    def test_failure(self) -> None:
         result = Mock()
 
-        exception = Exception()
+        class CustomException(Exception):
+            pass
+
+        exception = CustomException()
 
-        def streaming_content():
+        def streaming_content() -> Generator[Any, None, None]:
             self.logger.info("streaming_content")
             yield result
             raise exception
@@ -963,14 +1024,16 @@ class TestSyncStreamingContentWrapper(TestCase):
         wrapped_streaming_content = sync_streaming_content_wrapper(
             streaming_content(), {"foo": "bar"}
         )
-        with self.assertLogs(
-            __name__, logging.INFO
-        ) as streaming_content_log_results, self.assertLogs(
-            "django_structlog.middlewares.request", logging.INFO
-        ) as log_results:
+        with (
+            self.assertLogs(__name__, logging.INFO) as streaming_content_log_results,
+            self.assertLogs(
+                "django_structlog.middlewares.request", logging.INFO
+            ) as log_results,
+        ):
             self.assertEqual(result, next(wrapped_streaming_content))
 
         self.assertEqual(1, len(log_results.records))
+        record: Any
         record = log_results.records[0]
         self.assertEqual("INFO", record.levelname)
         self.assertEqual("streaming_started", record.msg["event"])
@@ -980,7 +1043,7 @@ class TestSyncStreamingContentWrapper(TestCase):
         with self.assertLogs(
             "django_structlog.middlewares.request", logging.INFO
         ) as log_results:
-            self.assertRaises(Exception, next, wrapped_streaming_content)
+            self.assertRaises(CustomException, next, wrapped_streaming_content)
 
         self.assertEqual(1, len(streaming_content_log_results.records))
         record = streaming_content_log_results.records[0]
@@ -997,27 +1060,29 @@ class TestSyncStreamingContentWrapper(TestCase):
 
 
 class TestASyncStreamingContentWrapper(TestCase):
-    def setUp(self):
+    def setUp(self) -> None:
         self.logger = structlog.getLogger(__name__)
 
-    async def test_success(self):
+    async def test_success(self) -> None:
         result = Mock()
 
-        async def streaming_content():
+        async def streaming_content() -> AsyncGenerator[Any, None]:
             self.logger.info("streaming_content")
             yield result
 
         wrapped_streaming_content = async_streaming_content_wrapper(
             streaming_content(), {"foo": "bar"}
         )
-        with self.assertLogs(
-            __name__, logging.INFO
-        ) as streaming_content_log_results, self.assertLogs(
-            "django_structlog.middlewares.request", logging.INFO
-        ) as log_results:
+        with (
+            self.assertLogs(__name__, logging.INFO) as streaming_content_log_results,
+            self.assertLogs(
+                "django_structlog.middlewares.request", logging.INFO
+            ) as log_results,
+        ):
             self.assertEqual(result, await wrapped_streaming_content.__anext__())
 
         self.assertEqual(1, len(log_results.records))
+        record: Any
         record = log_results.records[0]
         self.assertEqual("INFO", record.levelname)
         self.assertEqual("streaming_started", record.msg["event"])
@@ -1043,12 +1108,15 @@ class TestASyncStreamingContentWrapper(TestCase):
         self.assertIn("foo", record.msg)
         self.assertEqual("bar", record.msg["foo"])
 
-    async def test_failure(self):
+    async def test_failure(self) -> None:
         result = Mock()
 
-        exception = Exception()
+        class CustomException(Exception):
+            pass
 
-        async def streaming_content():
+        exception = CustomException()
+
+        async def streaming_content() -> AsyncGenerator[Any, None]:
             self.logger.info("streaming_content")
             yield result
             raise exception
@@ -1056,14 +1124,16 @@ class TestASyncStreamingContentWrapper(TestCase):
         wrapped_streaming_content = async_streaming_content_wrapper(
             streaming_content(), {"foo": "bar"}
         )
-        with self.assertLogs(
-            __name__, logging.INFO
-        ) as streaming_content_log_results, self.assertLogs(
-            "django_structlog.middlewares.request", logging.INFO
-        ) as log_results:
+        with (
+            self.assertLogs(__name__, logging.INFO) as streaming_content_log_results,
+            self.assertLogs(
+                "django_structlog.middlewares.request", logging.INFO
+            ) as log_results,
+        ):
             self.assertEqual(result, await wrapped_streaming_content.__anext__())
 
         self.assertEqual(1, len(log_results.records))
+        record: Any
         record = log_results.records[0]
         self.assertEqual("INFO", record.levelname)
         self.assertEqual("streaming_started", record.msg["event"])
@@ -1073,7 +1143,7 @@ class TestASyncStreamingContentWrapper(TestCase):
         with self.assertLogs(
             "django_structlog.middlewares.request", logging.INFO
         ) as log_results:
-            with self.assertRaises(StopAsyncIteration):
+            with self.assertRaises(CustomException):
                 await wrapped_streaming_content.__anext__()
 
         self.assertEqual(1, len(streaming_content_log_results.records))
@@ -1089,12 +1159,12 @@ class TestASyncStreamingContentWrapper(TestCase):
         self.assertIn("foo", record.msg)
         self.assertEqual("bar", record.msg["foo"])
 
-    async def test_cancel(self):
+    async def test_cancel(self) -> None:
         result = Mock()
 
         exception = asyncio.CancelledError()
 
-        async def streaming_content():
+        async def streaming_content() -> AsyncGenerator[Any, None]:
             self.logger.info("streaming_content")
             yield result
             raise exception
@@ -1111,6 +1181,7 @@ class TestASyncStreamingContentWrapper(TestCase):
                 await wrapped_streaming_content.__anext__()
 
         self.assertEqual(1, len(streaming_content_log_results.records))
+        record: Any
         record = streaming_content_log_results.records[0]
         self.assertEqual("INFO", record.levelname)
         self.assertIn("foo", record.msg)
diff --git a/test_app/tests/test_app_settings.py b/test_app/tests/test_app_settings.py
index 08c83b62fbde7de5688d2b141f26d0253447ac63..5a8469fbe1a6d94031a566acbd4f10d3857f9474 100644
--- a/test_app/tests/test_app_settings.py
+++ b/test_app/tests/test_app_settings.py
@@ -4,13 +4,13 @@ from django_structlog import app_settings
 
 
 class TestAppSettings(TestCase):
-    def test_celery_enabled(self):
+    def test_celery_enabled(self) -> None:
         settings = app_settings.AppSettings()
 
         with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=True):
             self.assertTrue(settings.CELERY_ENABLED)
 
-    def test_celery_disabled(self):
+    def test_celery_disabled(self) -> None:
         settings = app_settings.AppSettings()
 
         with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=False):
diff --git a/test_app/tests/test_apps.py b/test_app/tests/test_apps.py
index 5efbe8bc8792c70c85dc21e0ddddeee6cce76da6..bc985b80aac76aa54f1c10838890c5f1383d1c92 100644
--- a/test_app/tests/test_apps.py
+++ b/test_app/tests/test_apps.py
@@ -1,4 +1,4 @@
-from unittest.mock import patch, create_autospec
+from unittest.mock import create_autospec, patch
 
 from django.test import TestCase
 
@@ -7,7 +7,7 @@ from django_structlog.celery import receivers
 
 
 class TestAppConfig(TestCase):
-    def test_celery_enabled(self):
+    def test_celery_enabled(self) -> None:
         app = apps.DjangoStructLogConfig(
             "django_structlog", __import__("django_structlog")
         )
@@ -23,7 +23,7 @@ class TestAppConfig(TestCase):
         self.assertTrue(hasattr(app, "_celery_receiver"))
         self.assertIsNotNone(app._celery_receiver)
 
-    def test_celery_disabled(self):
+    def test_celery_disabled(self) -> None:
         app = apps.DjangoStructLogConfig(
             "django_structlog", __import__("django_structlog")
         )
@@ -39,7 +39,7 @@ class TestAppConfig(TestCase):
 
         self.assertFalse(hasattr(app, "_celery_receiver"))
 
-    def test_command_enabled(self):
+    def test_command_enabled(self) -> None:
         app = apps.DjangoStructLogConfig(
             "django_structlog", __import__("django_structlog")
         )
@@ -55,7 +55,7 @@ class TestAppConfig(TestCase):
         self.assertTrue(hasattr(app, "_django_command_receiver"))
         self.assertIsNotNone(app._django_command_receiver)
 
-    def test_command_disabled(self):
+    def test_command_disabled(self) -> None:
         app = apps.DjangoStructLogConfig(
             "django_structlog", __import__("django_structlog")
         )
diff --git a/test_app/tests/test_commands.py b/test_app/tests/test_commands.py
index c99ac96abcc862f1d4aabc444664cd15e3f144d8..5fca0a9521fcc2207ee88c1a8f75c53778ef09bc 100644
--- a/test_app/tests/test_commands.py
+++ b/test_app/tests/test_commands.py
@@ -1,26 +1,32 @@
 import logging
+from typing import Any
 
 import structlog
-from django.test import TestCase
 from django.core.management import BaseCommand, call_command
-from django_extensions.management.utils import signalcommand
+from django.test import TestCase
+from django_extensions.management.utils import (  # type: ignore[import-untyped]
+    signalcommand,
+)
 
 
 class TestCommands(TestCase):
-    def test_command(self):
+    def test_command(self) -> None:
         class Command(BaseCommand):
-            @signalcommand
-            def handle(self, *args, **options):
+
+            @signalcommand  # type: ignore[misc]
+            def handle(self, *args: Any, **options: Any) -> Any:
                 structlog.getLogger("command").info("command_event")
 
-        with self.assertLogs(
-            "command", logging.INFO
-        ) as command_log_results, self.assertLogs(
-            "django_structlog.commands", logging.INFO
-        ) as django_structlog_commands_log_results:
+        with (
+            self.assertLogs("command", logging.INFO) as command_log_results,
+            self.assertLogs(
+                "django_structlog.commands", logging.INFO
+            ) as django_structlog_commands_log_results,
+        ):
             call_command(Command())
 
         self.assertEqual(1, len(command_log_results.records))
+        record: Any
         record = command_log_results.records[0]
         self.assertEqual("command_event", record.msg["event"])
         self.assertIn("command_id", record.msg)
@@ -33,34 +39,34 @@ class TestCommands(TestCase):
         self.assertEqual("command_finished", record.msg["event"])
         self.assertIn("command_id", record.msg)
 
-    def test_nested_command(self):
+    def test_nested_command(self) -> None:
         class Command(BaseCommand):
-            @signalcommand
-            def handle(self, *args, **options):
+            @signalcommand  # type: ignore[misc]
+            def handle(self, *args: Any, **options: Any) -> None:
                 logger = structlog.getLogger("command")
                 logger.info("command_event_1")
                 call_command(NestedCommand())
                 logger.info("command_event_2")
 
         class NestedCommand(BaseCommand):
-            @signalcommand
-            def handle(self, *args, **options):
+            @signalcommand  # type: ignore[misc]
+            def handle(self, *args: Any, **options: Any) -> None:
                 structlog.getLogger("nested_command").info("nested_command_event")
 
-        with self.assertLogs(
-            "command", logging.INFO
-        ) as command_log_results, self.assertLogs(
-            "nested_command", logging.INFO
-        ), self.assertLogs(
-            "django_structlog.commands", logging.INFO
-        ) as django_structlog_commands_log_results:
+        with (
+            self.assertLogs("command", logging.INFO) as command_log_results,
+            self.assertLogs("nested_command", logging.INFO),
+            self.assertLogs(
+                "django_structlog.commands", logging.INFO
+            ) as django_structlog_commands_log_results,
+        ):
             call_command(Command())
 
         self.assertEqual(2, len(command_log_results.records))
-        command_event_1 = command_log_results.records[0]
+        command_event_1: Any = command_log_results.records[0]
         self.assertEqual("command_event_1", command_event_1.msg["event"])
         self.assertIn("command_id", command_event_1.msg)
-        command_event_2 = command_log_results.records[1]
+        command_event_2: Any = command_log_results.records[1]
         self.assertEqual("command_event_2", command_event_2.msg["event"])
         self.assertIn("command_id", command_event_2.msg)
         self.assertEqual(
@@ -68,11 +74,11 @@ class TestCommands(TestCase):
         )
 
         self.assertEqual(4, len(django_structlog_commands_log_results.records))
-        command_started_1 = django_structlog_commands_log_results.records[0]
+        command_started_1: Any = django_structlog_commands_log_results.records[0]
         self.assertEqual("command_started", command_started_1.msg["event"])
         self.assertIn("command_id", command_started_1.msg)
 
-        command_started_2 = django_structlog_commands_log_results.records[1]
+        command_started_2: Any = django_structlog_commands_log_results.records[1]
         self.assertEqual("command_started", command_started_2.msg["event"])
         self.assertIn("command_id", command_started_2.msg)
         self.assertIn("parent_command_id", command_started_2.msg)
@@ -81,7 +87,7 @@ class TestCommands(TestCase):
             command_started_2.msg["parent_command_id"],
         )
 
-        command_finished_1 = django_structlog_commands_log_results.records[2]
+        command_finished_1: Any = django_structlog_commands_log_results.records[2]
         self.assertEqual("command_finished", command_finished_1.msg["event"])
         self.assertIn("command_id", command_finished_1.msg)
         self.assertIn("parent_command_id", command_finished_1.msg)
@@ -90,7 +96,7 @@ class TestCommands(TestCase):
             command_finished_1.msg["parent_command_id"],
         )
 
-        command_finished_2 = django_structlog_commands_log_results.records[3]
+        command_finished_2: Any = django_structlog_commands_log_results.records[3]
         self.assertEqual("command_finished", command_finished_2.msg["event"])
         self.assertIn("command_id", command_finished_2.msg)
         self.assertNotIn("parent_command_id", command_finished_2.msg)