In Django, which middleware see the response when one returns early from __call__ versus from process_view?
answer
- the list order is the nesting
- where process_view actually runs
- the response retraces the way in
- exceptions become responses between layers
basics
~20 sAn early return from call is seen only by that middleware and those listed above it; lower layers and the view never run. A response from process_view skips the view, yet every middleware's call after-phase still sees it.
solid answer
~50 sDjango builds the chain from `MIDDLEWARE` so the first entry is the outermost layer: requests go down the list, responses come back up. If middleware B returns a response from `__call__` without calling `get_response`, everything below B, including every `process_view()` hook and the view, never sees the request, and the response travels back only through the layers above B. `process_view()` is different: it runs in the innermost handler, after the URL is resolved and after every `__call__` has already passed the request down, so a response returned there skips the remaining `process_view()` hooks and the view, but every middleware's after-phase still processes it. Between layers Django also converts exceptions to responses, so an outer middleware gets a 4xx or 500 response, not the exception. For a timing middleware this means it cannot time a redirect returned by a layer listed above it.
code
python · 15 linesfrom django.http import HttpResponseForbidden
class InternalOnlyMiddleware:
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):
# Runs after URL resolution, once every __call__ has passed the request in.
if getattr(view_func, "internal_only", False) and not request.user.is_staff:
return HttpResponseForbidden("Internal endpoint")
return Nonego deeper
Recall that the first MIDDLEWARE entry sees the request first and the response last, and that returning a response without calling get_response stops the request there.
Explain why process_view runs inside the innermost handler, and how that makes its early return visible to every after-phase while a call early return is not.
Use this to place cross-cutting middleware correctly, predict which requests a timing or tagging layer will miss, and explain why error hooks never see middleware exceptions.
Decide which checks belong in call and which in process_view across a shared middleware stack, so teams do not accidentally hide requests from observability layers.
## How the list becomes a chain Django's handler builds the middleware chain once, in `load_middleware()`. It walks `settings.MIDDLEWARE` **in reverse**, and each factory receives the handler built so far as its `get_response`. The result is that the **first** entry in the list is the **outermost** layer: - on the way in, requests pass the entries top to bottom; - on the way out, responses pass them bottom to top; - at the centre is not the view itself but a handler method that resolves the URL, runs the `process_view()` hooks, calls the view, and applies `process_exception()` and `process_template_response()`. That last point is what makes the two kinds of early return behave differently. ## Early return from `__call__` Suppose `MIDDLEWARE` lists A, B, C and B's `__call__` returns a response without calling `self.get_response(request)`. 1. A's before-phase runs and calls its `get_response`, which is B. 2. B returns a response straight away. 3. C's `__call__` never runs, the URL is never resolved, no `process_view()` hook runs, and the view is never called. 4. The response returns through A's after-phase only. A real example: with `SECURE_SSL_REDIRECT` enabled, `SecurityMiddleware` returns a permanent redirect to HTTPS before anything below it runs. A timing middleware listed below `SecurityMiddleware` never sees those requests; one listed above it times them. ## Early return from `process_view()` `process_view(request, view_func, view_args, view_kwargs)` hooks run inside the innermost handler, after every `__call__` before-phase has already handed the request down. When one returns an `HttpResponse`: 1. the remaining `process_view()` hooks (those lower in `MIDDLEWARE`) are skipped; 2. the view is not called; 3. `process_template_response()` hooks apply only if that response has a `render()` method; 4. the response then travels out through **every** middleware's after-phase, top of the list last. | Where the response comes from | Lower `__call__` layers | `process_view()` hooks | View | After-phases that run | |---|---|---|---|---| | B's `__call__`, no `get_response` call | skipped | none | skipped | layers above B | | a `process_view()` hook | already ran on the way in | the remaining ones skipped | skipped | all layers | | the view | ran | all ran | ran | all layers | ## Exceptions do not travel between layers Django wraps every layer, and the innermost handler, with `convert_exception_to_response`. If C raises inside its `__call__`, the wrapper turns the exception into a response (404 for `Http404`, 403 for `PermissionDenied`, 400 for the bad-request family, 500 for anything unknown) before B's `get_response` call returns. B therefore never needs `try/except` around `get_response`; it always gets a response. Two consequences: - `process_exception()` hooks are **not** called for exceptions raised in middleware; they only see exceptions from the view or from rendering a `TemplateResponse`. - The setting `DEBUG_PROPAGATE_EXCEPTIONS` (default `False`) makes uncaught exceptions propagate instead of becoming 500 responses, which is mainly useful in tests. ## Why this was not always so Under the old `MIDDLEWARE_CLASSES` setting, removed in Django 2.0, every middleware's `process_response()` ran even when an earlier middleware short-circuited, and an exception in a response hook skipped the rest and forced a 500. The current `MIDDLEWARE` design is a true onion: the layers a response passes on the way out are exactly the ones the request passed on the way in. ## Practical rules - Put a middleware that must see **every** response, such as timing or request-ID tagging, above anything that can return early. - Put a middleware that needs the resolved view in `process_view()`: that is the first point where `view_func` and `request.resolver_match` exist. - Remember that an early return from `__call__` also skips everyone's `process_view()`, including checks that rely on it.
- Why does a middleware's after-phase never need try/except around get_response?Django wraps each layer and the innermost handler with `convert_exception_to_response`, so any exception raised further in is turned into a response before `get_response` returns: 404, 403, 400 or 500 depending on the exception. The only exception is an uncaught error when `DEBUG_PROPAGATE_EXCEPTIONS` is `True`, which then propagates upward.
- Why is process_view the first place a middleware can see which view will run?URL resolution happens in the innermost handler, after every `__call__` before-phase. Only there does Django know `view_func`, its arguments and `request.resolver_match`, and it calls the `process_view()` hooks with them just before the view. Code in `__call__` before `get_response` runs too early to know the view.
The middleware list is a set of nested doors to a room. Someone turned away at the second door only walks back out through the first; nobody at the inner doors ever meets them. process_view is a desk inside the innermost door: a visitor refused there has already passed every door, so they walk back out through all of them.
saying these in an interview costs you the question
- Every middleware's after-phase runs even when a higher middleware returns early.
- A response from process_view is seen only by middleware listed above the one that returned it.
- An exception in an inner middleware reaches the outer one as an exception.
- process_exception is called for exceptions raised inside other middleware.
- The last entry in MIDDLEWARE sees the request first.