In Django, what happens, in order, between WSGIHandler receiving a request for /recipes/42/ and the HttpResponse going back to the server?
answer
- chain built once, walked per request
- request_started, then the request object
- resolution happens innermost
- view must return a response
- response unwinds the middleware
basics
~10 sWSGIHandler builds an HttpRequest and passes it down the MIDDLEWARE chain; innermost, the resolver matches the path against ROOT_URLCONF and the view returns an HttpResponse, which travels back up the middleware in reverse order.
solid answer
~40 sAt startup `get_wsgi_application()` runs `django.setup()` and builds a `WSGIHandler`, whose `load_middleware()` wraps the `MIDDLEWARE` classes into one chain. Per request, `WSGIHandler.__call__` sends `request_started`, builds a `WSGIRequest` from the environ and calls `get_response()`, which sets the thread's URLconf to `ROOT_URLCONF` and enters the chain. Each middleware runs its request-side code top to bottom. At the centre, `resolve_request()` resolves `request.path_info` (using `request.urlconf` if a middleware set one) and stores `request.resolver_match`; view middleware runs; the view is called and must return an `HttpResponse` — `None` raises a `ValueError`. A `TemplateResponse` is rendered there. The response then passes back through the middleware bottom to top, and the handler calls `start_response`. `request_finished` fires when the server closes the response.
code
python · 17 lines# recipes/views.py
from django.shortcuts import get_object_or_404, render
from .models import Recipe
def detail(request, pk):
recipe = get_object_or_404(Recipe, pk=pk) # raises Http404 -> 404 response
return render(request, "recipes/detail.html", {"recipe": recipe})
# recipes/urls.py
from django.urls import path
from . import views
urlpatterns = [path("<int:pk>/", views.detail, name="recipe-detail")]go deeper
Recall the order: request object, middleware down, URL resolver, view, response back up through middleware. A view must return an HttpResponse.
Explain that the chain is built once by load_middleware, that resolution runs innermost, and that exceptions become responses at each layer.
Use the path to diagnose production puzzles: why a middleware sees 404s for bad URLs, why process_view never fired, where ATOMIC_REQUESTS wraps the view.
Decide which cross-cutting concerns belong in middleware versus views or decorators, weighing that middleware runs on every request, including 404s and static misses.
## Two phases: once per process, then per request Django's request path has a **setup phase** and a **per-request phase**. In `wsgi.py`, `application = get_wsgi_application()` calls `django.setup()` — load settings, configure logging, populate the app registry — and then constructs a `WSGIHandler`. The handler's constructor calls `load_middleware()`, which walks `MIDDLEWARE` from the **last** entry to the first, wrapping each middleware around the one inside it, so the first entry ends up outermost. The result is a single callable chain whose innermost layer is the handler's own `_get_response()`. `ASGIHandler`, built by `get_asgi_application()` in `asgi.py`, follows the same shape with `load_middleware(is_async=True)`; it accepts only `http` scopes and raises `ValueError` for anything else. ## The per-request steps For `GET /recipes/42/` on a recipe-sharing site, the WSGI server calls `WSGIHandler(environ, start_response)`: 1. **Script prefix and signal** — `set_script_prefix()` records the mount point, and the `request_started` signal is sent. 2. **Request object** — a `WSGIRequest` (an `HttpRequest` subclass) is built from the environ. 3. **URLconf for this thread** — `get_response()` calls `set_urlconf(settings.ROOT_URLCONF)` and enters the middleware chain. 4. **Middleware, request side** — each middleware's code before `get_response(request)` runs, in `MIDDLEWARE` order, top to bottom. Any layer may return a response early and skip everything inside it. 5. **URL resolution** — at the centre, `resolve_request()` uses `request.urlconf` if a middleware set it, otherwise the root URLconf, calls `resolver.resolve(request.path_info)` and stores the `ResolverMatch` on `request.resolver_match`. A path with no match raises `Resolver404` here. 6. **View middleware** — `process_view` hooks run with the resolved view and its arguments; one returning a response short-circuits the view. 7. **The view** — the callable runs, wrapped in `transaction.atomic` if the database has `ATOMIC_REQUESTS` enabled. An exception goes to `process_exception` hooks. 8. **Response check** — if the view returned `None`, Django raises `ValueError("The view recipes.views.detail didn't return an HttpResponse object. It returned None instead.")`. 9. **Deferred rendering** — if the response has a callable `render()` (a `TemplateResponse`, as returned by `TemplateView`), `process_template_response` hooks run and then the template is rendered. The `render()` shortcut returns an already-rendered `HttpResponse`, so it skips this step. 10. **Middleware, response side** — the response travels back out through each middleware's code after `get_response()`, bottom to top. 11. **Back to the server** — the handler calls `start_response(status, headers)` (cookies included) and returns the response as the body iterable. When the server calls `close()`, `request_finished` is sent. ## Where errors turn into responses `load_middleware()` wraps **every** layer, including the innermost one, with `convert_exception_to_response()`. So `Http404`, `Resolver404`, `PermissionDenied` and other exceptions become responses at the layer where they are raised, and outer middleware sees an ordinary 404 or 403 **response**, not an exception. | Event | Where it happens | What the outer middleware sees | |---|---|---| | no URL pattern matches | step 5, innermost layer | a 404 response; `process_view` never ran | | view raises `Http404` | step 7 | a 404 response | | view returns `None` | step 8 | a 500 response (or the debug page with `DEBUG=True`) | | a middleware returns early | step 4 | only the layers outside it run | ## What the newcomer should take away - Middleware wraps **both** directions, and every middleware runs even for a 404. - URL resolution happens **inside** the chain, so a middleware can change `request.urlconf` before resolution. - A view's only contract is to return an `HttpResponse` (or subclass); templates are optional. - Signals bracket the request: `request_started` before, `request_finished` on close. The detailed semantics of each middleware hook and of pattern matching belong to their own topics; this path is the map that places them.
- Does every middleware still run when the URL matches no pattern?Yes. Resolution happens inside the innermost layer, after every middleware's request-side code has run. `Resolver404` is turned into a 404 response there by `convert_exception_to_response()`, so each middleware's response-side code runs on that 404. Only `process_view` hooks are skipped, because resolution failed before they are called.
- How can one project serve a different URLconf per request?A middleware can set `request.urlconf` to a module path before resolution. `resolve_request()` checks for that attribute and uses its resolver instead of `ROOT_URLCONF`; `set_urlconf()` also makes `reverse()` use it for the rest of that request.
- What changes on this path under ASGIHandler?The shape stays: `request_started`, request object, middleware chain, resolution, view, response. The chain is built with `is_async=True`, so sync middleware and sync views are adapted to run in a thread, and the handler only accepts HTTP scopes. `ATOMIC_REQUESTS` raises `RuntimeError` for an async view.
saying these in an interview costs you the question
- Says URL resolution happens before any middleware runs
- Believes middleware only sees requests, not responses
- Thinks a 404 skips all middleware entirely
- Expects a view returning None to produce an empty 200 response
- Says the middleware chain is rebuilt for every request
- Claims render() returns a TemplateResponse rendered later