The request/response lifecycle

Django · Expert · 14 min

Knowing each step lets you put code in the right layer and debug problems that aren't in your view.

What happens between the server and your view:

1. The server calls the handler: WSGIHandler (from wsgi.py) or ASGIHandler (from asgi.py). At startup the handler instantiated every middleware once, building the chain from MIDDLEWARE. 2. request_started is sent, and the handler builds an HttpRequest (WSGIRequest or ASGIRequest) from the environ or scope. 3. The request passes down the middleware chain. Each layer is wrapped by convert_exception_to_response, so an exception raised anywhere becomes a response at that layer instead of escaping. 4. At the bottom, _get_response resolves the URL: the resolver for ROOT_URLCONF (or a per-request request.urlconf) returns a ResolverMatch with the view, args, kwargs and URL name, stored as request.resolver_match. 5. Middleware process_view hooks run, then the view is called (wrapped with atomic if ATOMIC_REQUESTS is set). 6. If the view returns a TemplateResponse, process_template_response hooks run and the template is rendered lazily now, which is why middleware can still change its context. 7. The response travels up the chain. Exceptions trigger process_exception hooks, then Django's handlers: Http404 becomes a 404 (via handler404), PermissionDenied a 403, SuspiciousOperation a 400, other errors a 500 (via handler500) and the error is logged to django.request. 8. The server sends the response; when it closes it, request_finished fires and Django closes database connections that exceeded CONN_MAX_AGE.

Streaming and file responses send their body after the view returns, so code relying on request-scoped state there must be careful.

Example

# Inspecting the lifecycle from inside a project (e.g. a test or shell)
from django.core.signals import request_finished, request_started
from django.test import Client
from django.urls import resolve

match = resolve("/blog/posts/3/")
print(match.view_name, match.kwargs, match.func)    # blog:detail {'pk': 3} <function post_detail ...>

events = []
request_started.connect(lambda **kw: events.append("started"))
request_finished.connect(lambda **kw: events.append("finished"))

class ShowMatch:                                    # a middleware using process_view
    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):
        events.append(f"view {request.resolver_match.view_name}")

# with ShowMatch in MIDDLEWARE:
resp = Client().get("/blog/posts/3/")
print(events)        # ['started', 'view blog:detail', 'finished']

Remember

  • Handler builds the request, then middleware runs down, the view runs, the response goes up
  • The URL resolver produces request.resolver_match
  • Each middleware layer converts exceptions into responses
  • TemplateResponse renders lazily after the view returns

Watch out for

  • Expecting process_view hooks to run for requests that a middleware above short-circuited.

Check yourself

  1. A view returns a TemplateResponse. When is its template rendered?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/ref/request-response/