ZetCode

Django middleware

last modified November 13, 2026 Python 3.15

Middleware is code that sits between the web server and the view. Every request passes through a chain of middleware on its way to the view, and every response passes back through the same chain in the opposite order. This makes middleware the right place for work that concerns all requests: adding headers, measuring time, authenticating users, handling sessions, compressing responses, or turning exceptions into responses. In this article we write function-based and class-based middleware, watch the order in which the pieces are called, use the special hooks of class-based middleware, write a middleware that works in sync and async mode, look at the built-in SecurityMiddleware, and test it all. The examples use Django 6.1 and Python 3.15.

How middleware works

The setting MIDDLEWARE is a list of dotted paths. At startup Django builds the chain from the last item to the first: each middleware is created with the next one as its argument get_response, so calling get_response(request) passes the request on to the next layer, and finally to the view. The first item in the list is the outermost layer: it sees the request first and the response last. A middleware may do something before calling get_response, something after it returns, or not call it at all and return a response of its own, which is called short-circuiting. Django accepts two forms: a function that returns an inner function, and a class with __init__(get_response) and __call__(request). The class form may add optional hook methods that Django calls at special moments. The middleware is created once, when the first request arrives, and the same object serves all requests, so it must not keep per-request data in its attributes.

The example project

The project is created with uv; the steps are explained in Django introduction. The commands below create a project directory, install Django, generate the project with the configuration package config, start the app pages, and create the folder demos for demo scripts.

$ mkdir middleware
$ cd middleware
$ uv init --bare
$ uv add django
$ uv run django-admin startproject config .
$ uv run python manage.py startapp pages
$ mkdir demos

The trailing dot in startproject config . creates manage.py and the config package in the current directory instead of one level deeper. A new app is ignored by Django until it is listed in INSTALLED_APPS, so we add the app to the list in config/settings.py; the other entries are generated by startproject.

config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pages',
]

The listings in this article are of two kinds. A listing with a solid blue border and the label Project file is a file of the application: models, views, templates, tests, and settings. A listing with a dashed amber border and the label Demo script is a small helper in the demos folder; it sets up Django, runs a few lines of code, and prints the results, so that we can watch the behaviour. Demo scripts are not part of the application; we run one from the project directory with uv run python -m demos.name.

The app pages has no models. Its views exist only to be wrapped by middleware: index uses the attribute that our first middleware adds to the request, plain and health are trivial, article returns a TemplateResponse, and the others fail in three different ways.

pages/views.py
from django.http import Http404, HttpResponse
from django.template.response import TemplateResponse

from .hooks import ApiError


def index(request):
    return HttpResponse(f'Hello, request {request.request_id}')


def plain(request):
    return HttpResponse('plain')


def health(request):
    return HttpResponse('ok')


def article(request, slug):
    return TemplateResponse(request, 'pages/article.html', {'slug': slug})


def boom(request):
    raise ApiError('the quota is exhausted', status=429)


def crash(request):
    raise RuntimeError('unexpected')


def missing(request):
    raise Http404('no such page')

The view boom raises our own exception ApiError, crash raises an unexpected RuntimeError, and missing raises Http404. The exception class is defined in pages/hooks.py, which we write in a later section. The URL patterns connect the paths with the views.

pages/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index),
    path('plain/', views.plain),
    path('health/', views.health),
    path('articles/<slug:slug>/', views.article),
    path('boom/', views.boom),
    path('crash/', views.crash),
    path('missing/', views.missing),
]

The project URL configuration includes the patterns of the app at the root.

config/urls.py
from django.urls import include, path

urlpatterns = [
    path('', include('pages.urls')),
]

The template of the article view prints a variable, view_name, that no view sets; a middleware will add it.

pages/templates/pages/article.html
<h1>{{ slug }}</h1>
<p>Rendered by {{ view_name }}</p>

The template is a short fragment, not a full page, which is enough for the demonstration.

Function and class middleware

The first middleware is a function. request_id_middleware takes get_response and returns the function middleware, which is called for every request. It reads the header X-Request-ID or generates a short identifier, stores it on the request so that the view can use it, calls the next layer, and copies the identifier to the response. A proxy in front of the application can send the same header, so the identifier then follows a request through several systems. The class TimingMiddleware does the same in the class form: __init__ keeps get_response, and __call__ measures the time around the call. The third middleware, MaintenanceMiddleware, returns a response of its own, 503 Service Unavailable, when the setting MAINTENANCE_MODE is on, except for the path /health/, which a load balancer keeps checking. It reads the setting in __call__, not in __init__, so the setting can change while the process runs.

pages/middleware.py
import time
import uuid

from django.conf import settings
from django.http import HttpResponse


def request_id_middleware(get_response):
    def middleware(request):
        request.request_id = request.headers.get('X-Request-ID') or uuid.uuid4().hex[:8]
        response = get_response(request)
        response['X-Request-ID'] = request.request_id
        return response

    return middleware


class TimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        start = time.perf_counter()
        response = self.get_response(request)
        elapsed = (time.perf_counter() - start) * 1000
        response['X-Duration-ms'] = f'{elapsed:.1f}'
        return response


class MaintenanceMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if settings.MAINTENANCE_MODE and not request.path.startswith('/health/'):
            return HttpResponse('Back soon.', status=503, headers={'Retry-After': '120'})
        return self.get_response(request)

Middleware is switched on by listing it in MIDDLEWARE. We insert ours after SecurityMiddleware, which stays first, and add the setting MAINTENANCE_MODE at the end of the settings file; the middleware from the later sections is already in the list. The order is significant, and we return to it in the next section.

config/settings.py (middleware)
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'pages.middleware.request_id_middleware',
    'pages.middleware.TimingMiddleware',
    'pages.middleware.MaintenanceMiddleware',
    'pages.hooks.ApiErrorMiddleware',
    'pages.hooks.ViewInfoMiddleware',
    'pages.modes.DebugOnlyMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

...

MAINTENANCE_MODE = False

The demo script prints the list, requests the index page, and toggles the maintenance mode with override_settings. The identifier in the response is the one the view printed. A request that carries its own X-Request-ID keeps it. In maintenance mode the page returns 503 with the header Retry-After, and /health/ still answers 200.

demos/basics.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.test import Client
from django.test.utils import setup_test_environment

setup_test_environment()

import logging

from django.conf import settings
from django.test import override_settings

logging.getLogger('django.request').setLevel(logging.ERROR)

client = Client()
print('MIDDLEWARE:')
for path in settings.MIDDLEWARE:
    print('  ', path)
print()

response = client.get('/')
print(response.status_code, response.content.decode())
print('X-Request-ID:', response['X-Request-ID'])
print('X-Duration-ms:', response['X-Duration-ms'])
print('sent X-Request-ID is kept:', client.get('/', headers={'X-Request-ID': 'abc123'})['X-Request-ID'])
print()

with override_settings(MAINTENANCE_MODE=True):
    for path in ('/', '/health/'):
        response = client.get(path)
        print(f'maintenance, GET {path} -> {response.status_code}', response.get('Retry-After', ''))

The page and the response header have the same identifier, and the time is the duration of everything inside the middleware, in milliseconds.

$ uv run python -m demos.basics
MIDDLEWARE:
   django.middleware.security.SecurityMiddleware
   pages.middleware.request_id_middleware
   pages.middleware.TimingMiddleware
   pages.middleware.MaintenanceMiddleware
   pages.hooks.ApiErrorMiddleware
   pages.hooks.ViewInfoMiddleware
   pages.modes.DebugOnlyMiddleware
   django.contrib.sessions.middleware.SessionMiddleware
   django.middleware.common.CommonMiddleware
   django.middleware.csrf.CsrfViewMiddleware
   django.contrib.auth.middleware.AuthenticationMiddleware
   django.contrib.messages.middleware.MessageMiddleware
   django.middleware.clickjacking.XFrameOptionsMiddleware

200 Hello, request 05aabd1b
X-Request-ID: 05aabd1b
X-Duration-ms: 3.1
sent X-Request-ID is kept: abc123

maintenance, GET / -> 503 120
maintenance, GET /health/ -> 200 
Service Unavailable: /

The order of middleware

The request travels from the first item of MIDDLEWARE to the last, and the response travels back. To see this we use three classes that record what happens in a list. The base class Tracer records the call of __call__, before and after the inner call, and the hooks process_view and process_exception, which are described in the next section. The class Gate returns a 403 response without calling the inner layers when the query string contains stop.

pages/tracing.py
from django.http import HttpResponse

TRACE = []


class Tracer:
    name = '?'

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        TRACE.append(f'{self.name}: request')
        response = self.get_response(request)
        TRACE.append(f'{self.name}: response {response.status_code}')
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        TRACE.append(f'{self.name}: process_view')

    def process_exception(self, request, exception):
        TRACE.append(f'{self.name}: process_exception {type(exception).__name__}')


class First(Tracer):
    name = 'First'


class Gate(Tracer):
    name = 'Gate'

    def __call__(self, request):
        if 'stop' in request.GET:
            TRACE.append('Gate: request, answering without the view')
            return HttpResponse('Stopped', status=403)
        return super().__call__(request)


class Third(Tracer):
    name = 'Third'

The demo replaces MIDDLEWARE with the three tracers, in the order First, Gate, Third, and visits four URLs. Each visit gets a fresh test client, because the chain is built when a client handles its first request.

demos/order.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.test import Client
from django.test.utils import setup_test_environment

setup_test_environment()

from django.test import override_settings

import logging

from pages.tracing import TRACE

logging.getLogger('django.request').setLevel(logging.CRITICAL)

TRACING = ['pages.tracing.First', 'pages.tracing.Gate', 'pages.tracing.Third']


def visit(path, **options):
    TRACE.clear()
    client = Client(raise_request_exception=False)
    with override_settings(MIDDLEWARE=TRACING):
        response = client.get(path)
    print(f'GET {path} -> {response.status_code}')
    for line in TRACE:
        print('  ', line)
    print()


visit('/plain/')
visit('/plain/?stop=1')
visit('/missing/')
visit('/crash/')

For a normal request the first layer is the first to see the request and the last to see the response. The hook process_view is called in the same order, after the URL has been resolved and before the view runs. A short-circuit by Gate means that Third and the view are never reached, but the layers outside of Gate still see the response, here with the status 403. This is why security-related middleware, such as authentication and CSRF protection, stands before the code that depends on it. The hook process_exception is called in the reverse order, and it is called for Http404 too. Even in the case of an unhandled RuntimeError every layer receives a response, with the status 500, because Django turns the exception into a response before it reaches the middleware.

$ uv run python -m demos.order
GET /plain/ -> 200
   First: request
   Gate: request
   Third: request
   First: process_view
   Gate: process_view
   Third: process_view
   Third: response 200
   Gate: response 200
   First: response 200

GET /plain/?stop=1 -> 403
   First: request
   Gate: request, answering without the view
   First: response 403

GET /missing/ -> 404
   First: request
   Gate: request
   Third: request
   First: process_view
   Gate: process_view
   Third: process_view
   Third: process_exception Http404
   Gate: process_exception Http404
   First: process_exception Http404
   Third: response 404
   Gate: response 404
   First: response 404

GET /crash/ -> 500
   First: request
   Gate: request
   Third: request
   First: process_view
   Gate: process_view
   Third: process_view
   Third: process_exception RuntimeError
   Gate: process_exception RuntimeError
   First: process_exception RuntimeError
   Third: response 500
   Gate: response 500
   First: response 500

Hooks: process_view, process_exception, process_template_response

A class-based middleware may define up to three more methods, which Django discovers by name. process_view(request, view_func, view_args, view_kwargs) is called just before the view; it returns None to continue or a response to skip the view. It is the first place where we know which view is going to run. process_exception(request, exception) is called when the view raises an exception; it returns None to let other layers and the default handler decide, or a response. process_template_response(request, response) is called only for responses that have a render() method, such as TemplateResponse, after the view has returned and before the template is rendered, so it can still change the context.

The file pages/hooks.py defines two middleware. ApiErrorMiddleware converts our exception ApiError into a JSON response with the status carried by the exception, and ignores all other exceptions. ViewInfoMiddleware remembers the name of the view in process_view, and puts it into the context of template responses.

pages/hooks.py
from django.http import JsonResponse


class ApiError(Exception):
    def __init__(self, message, status=400):
        super().__init__(message)
        self.status = status


class ApiErrorMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_exception(self, request, exception):
        if isinstance(exception, ApiError):
            return JsonResponse({'error': str(exception)}, status=exception.status)
        return None


class ViewInfoMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        request.view_name = f'{view_func.__module__}.{view_func.__name__}'
        return None

    def process_template_response(self, request, response):
        response.context_data['view_name'] = request.view_name
        return response

The demo visits the article page, whose template shows the name added by the middleware, and the three failing views. The last lines remove ApiErrorMiddleware from the list to see the status without it.

demos/hooks.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.test import Client
from django.test.utils import setup_test_environment

setup_test_environment()

import logging

from django.conf import settings
from django.test import override_settings

logging.getLogger('django.request').setLevel(logging.CRITICAL)

client = Client(raise_request_exception=False)
print(client.get('/articles/middleware/').content.decode().strip())
print()

for path in ('/boom/', '/crash/', '/missing/'):
    response = client.get(path)
    print(f'GET {path} -> {response.status_code}', response['Content-Type'])
print('body of /boom/:', client.get('/boom/').content.decode())
print()

without = [m for m in settings.MIDDLEWARE if 'ApiErrorMiddleware' not in m]
with override_settings(MIDDLEWARE=without):
    response = Client(raise_request_exception=False).get('/boom/')
    print('without ApiErrorMiddleware: GET /boom/ ->', response.status_code)

The template prints the name of the view although the view did not pass it. The view boom gets the JSON answer with the status 429, while the same exception without the middleware produces the status 500. The other exceptions are untouched: Http404 leads to the 404 page and the RuntimeError to the 500 page. The test client usually re-raises exceptions from the view; raise_request_exception=False makes it return the 500 response, as a real server would.

$ uv run python -m demos.hooks
<h1>middleware</h1>
<p>Rendered by pages.views.article</p>

GET /boom/ -> 429 application/json
GET /crash/ -> 500 text/html
GET /missing/ -> 404 text/html; charset=utf-8
body of /boom/: {"error": "the quota is exhausted"}

without ApiErrorMiddleware: GET /boom/ -> 500

MiddlewareNotUsed and async middleware

A middleware can remove itself from the chain. If __init__ raises MiddlewareNotUsed, Django skips it, logs the reason when DEBUG is on, and builds the chain without it. This is useful for middleware that depends on a setting or an installed package. The class DebugOnlyMiddleware is used only when DEBUG is true.

Django can run under WSGI, synchronously, and under ASGI, asynchronously. A middleware that does not say anything about it is treated as synchronous, and Django adapts it, which costs a thread switch in an async project. A middleware that wants to work in both modes is decorated with sync_and_async_middleware (for a class there are the attributes sync_capable and async_capable), and it checks whether get_response is a coroutine function to decide which inner function to return. The function label_middleware sets the header X-Mode in the way that matches the mode.

pages/modes.py
from asyncio import iscoroutinefunction

from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
from django.utils.decorators import sync_and_async_middleware


class DebugOnlyMiddleware:
    def __init__(self, get_response):
        if not settings.DEBUG:
            raise MiddlewareNotUsed('only for development')
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        response['X-Debug-Mode'] = 'on'
        return response


@sync_and_async_middleware
def label_middleware(get_response):
    if iscoroutinefunction(get_response):
        async def middleware(request):
            response = await get_response(request)
            response['X-Mode'] = 'async'
            return response
    else:
        def middleware(request):
            response = get_response(request)
            response['X-Mode'] = 'sync'
            return response

    return middleware

The demo creates test clients under different settings. The chain is built by the client when it handles its first request, so a new client reads the changed value of DEBUG. The synchronous Client and the AsyncClient call the same decorated function and get the two variants.

demos/modes.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.test import Client
from django.test.utils import setup_test_environment

setup_test_environment()

import asyncio

from django.test import AsyncClient, override_settings

print('DEBUG=True :', Client().get('/plain/').get('X-Debug-Mode'))
with override_settings(DEBUG=False):
    print('DEBUG=False:', Client().get('/plain/').get('X-Debug-Mode'))
print()

with override_settings(MIDDLEWARE=['pages.modes.label_middleware']):
    print('sync client :', Client().get('/plain/')['X-Mode'])
    print('async client:', asyncio.run(AsyncClient().get('/plain/'))['X-Mode'])

The middleware that depends on DEBUG is active only while DEBUG is true, and the dual-mode middleware reports the mode of the request that it served.

$ uv run python -m demos.modes
DEBUG=True : on
DEBUG=False: None

sync client : sync
async client: async

The built-in middleware

The list that startproject generates contains the middleware that most projects need. SecurityMiddleware adds security headers and can redirect to HTTPS. SessionMiddleware and AuthenticationMiddleware set request.session and request.user; CommonMiddleware does the APPEND_SLASH redirect; CsrfViewMiddleware rejects unsafe requests without a token; MessageMiddleware supports the messages framework; and XFrameOptionsMiddleware sets X-Frame-Options against clickjacking. Optional ones include GZipMiddleware, ConditionalGetMiddleware and LocaleMiddleware.

SecurityMiddleware is controlled by settings with the prefix SECURE_. It reads them once, when it is created, unlike the maintenance middleware, so the demo uses a new client for each combination. By default it sets X-Content-Type-Options, Referrer-Policy, and Cross-Origin-Opener-Policy. SECURE_SSL_REDIRECT sends plain HTTP requests to HTTPS with a permanent redirect, and SECURE_HSTS_SECONDS adds Strict-Transport-Security, but only to responses to HTTPS requests. The last test removes XFrameOptionsMiddleware to show that the header X-Frame-Options belongs to it.

demos/security.py
import os

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()

from django.test import Client
from django.test.utils import setup_test_environment

setup_test_environment()

from django.conf import settings
from django.test import override_settings

client = Client()
names = ('Strict-Transport-Security', 'X-Content-Type-Options', 'Referrer-Policy',
         'Cross-Origin-Opener-Policy', 'X-Frame-Options')


def show(label, response):
    print(label, '->', response.status_code, response.get('Location', ''))
    for name in names:
        if name in response:
            print(f'   {name}: {response[name]}')


show('defaults', client.get('/'))
print()

with override_settings(SECURE_HSTS_SECONDS=31536000, SECURE_HSTS_INCLUDE_SUBDOMAINS=True,
                       SECURE_SSL_REDIRECT=True):
    client = Client()
    show('SECURE_SSL_REDIRECT, http', client.get('/'))
    show('SECURE_SSL_REDIRECT, https', client.get('/', secure=True))
print()

with override_settings(MIDDLEWARE=[m for m in settings.MIDDLEWARE if 'XFrame' not in m]):
    show('without XFrameOptionsMiddleware', Client().get('/'))

The HTTP request is redirected to the HTTPS address, and the HSTS header appears only on the secure response. Before enabling HSTS in production, make sure that HTTPS works for all subdomains, because browsers remember the policy for the given time. The command manage.py check --deploy lists such settings.

$ uv run python -m demos.security
defaults -> 200 
   X-Content-Type-Options: nosniff
   Referrer-Policy: same-origin
   Cross-Origin-Opener-Policy: same-origin
   X-Frame-Options: DENY

SECURE_SSL_REDIRECT, http -> 301 https://testserver/
   X-Content-Type-Options: nosniff
   Referrer-Policy: same-origin
   Cross-Origin-Opener-Policy: same-origin
SECURE_SSL_REDIRECT, https -> 200 
   Strict-Transport-Security: max-age=31536000; includeSubDomains
   X-Content-Type-Options: nosniff
   Referrer-Policy: same-origin
   Cross-Origin-Opener-Policy: same-origin
   X-Frame-Options: DENY

without XFrameOptionsMiddleware -> 200 
   X-Content-Type-Options: nosniff
   Referrer-Policy: same-origin
   Cross-Origin-Opener-Policy: same-origin

Testing middleware

A middleware that only wraps get_response is easy to test as a plain function or object. We pass a fake get_response and a request from RequestFactory, so no URL configuration or database is involved, which also lets us check that a short-circuiting middleware does not call the inner layer. The integration tests use the test client and see the full chain.

pages/tests.py
from django.http import HttpResponse
from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings

from .middleware import MaintenanceMiddleware, request_id_middleware


class MiddlewareUnitTests(SimpleTestCase):

    def test_request_id_is_generated(self):
        middleware = request_id_middleware(lambda request: HttpResponse())
        request = RequestFactory().get('/')
        response = middleware(request)
        self.assertEqual(response['X-Request-ID'], request.request_id)
        self.assertEqual(len(request.request_id), 8)

    def test_request_id_is_kept(self):
        middleware = request_id_middleware(lambda request: HttpResponse())
        request = RequestFactory().get('/', headers={'X-Request-ID': 'abc'})
        self.assertEqual(middleware(request)['X-Request-ID'], 'abc')

    @override_settings(MAINTENANCE_MODE=True)
    def test_maintenance_does_not_call_the_view(self):
        calls = []
        middleware = MaintenanceMiddleware(lambda request: calls.append(request))
        response = middleware(RequestFactory().get('/'))
        self.assertEqual(response.status_code, 503)
        self.assertEqual(calls, [])


class MiddlewareIntegrationTests(TestCase):

    def test_headers(self):
        response = self.client.get('/')
        self.assertEqual(len(response['X-Request-ID']), 8)
        self.assertIn('X-Duration-ms', response)

    def test_api_error_becomes_json(self):
        response = self.client.get('/boom/')
        self.assertEqual(response.status_code, 429)
        self.assertEqual(response.json(), {'error': 'the quota is exhausted'})

    def test_template_response_context(self):
        response = self.client.get('/articles/middleware/')
        self.assertContains(response, 'Rendered by pages.views.article')

    @override_settings(MAINTENANCE_MODE=True)
    def test_maintenance(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 503)
        self.assertEqual(response['Retry-After'], '120')

The unit tests check the generated and the preserved identifier and the short-circuit of the maintenance mode. The integration tests check the headers, the JSON error, the context added by the hook, and the maintenance page, with the settings changed by the decorator override_settings. All of them pass.

$ uv run python manage.py test
Found 7 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.006s

OK
Destroying test database for alias 'default'...

Source

Django documentation: Middleware

Django documentation: Middleware reference

Django documentation: MIDDLEWARE setting

In this article we have written function-based and class-based middleware, followed the request and the response through the chain, used the hooks process_view, process_exception, and process_template_response, written a middleware for both sync and async mode, configured SecurityMiddleware, and tested the code. Related articles are Django views, Django HttpResponse, and Django sessions and cookies.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Django tutorials.