Django 系列 · 第三篇

settings.MIDDLEWARE 里的顺序,是真的有讲究,还是随便排?

Django 中间件常被形容成"洋葱模型"——请求从外到内穿过每一层,响应再从内到外穿回去。这一篇本机真实验证了这个模型,顺着源码找到"洋葱"是怎么被真实拼装出来的(一个出人意料地简单的 reversed() 循环),还真实复现了一个因为中间件顺序写反而触发的真实报错——证明这个顺序真的不是随便排的。

A → B → C → view → C → B → A
本机真实抓到的完整请求/响应执行顺序
ImproperlyConfigured
本机真实复现:AuthenticationMiddleware 排在 SessionMiddleware 前面,真实报错

真实实测:洋葱模型,一字不差

写三个最简单的中间件,各自在请求阶段和响应阶段打一行日志,挂到 MIDDLEWARE = [A, B, C],真实发一个请求。

真实实测
def make_middleware(name):
    def middleware(get_response):
        def mw(request):
            print(f'  -> entering {name} (request phase)')
            response = get_response(request)
            print(f'  <- leaving  {name} (response phase)')
            return response
        return mw
    return middleware
# 挂载为 MIDDLEWARE = ['mw_test.MwA', 'mw_test.MwB', 'mw_test.MwC']
-> entering MwA (request phase) -> entering MwB (request phase) -> entering MwC (request phase) ** actual view executing ** <- leaving MwC (response phase) <- leaving MwB (response phase) <- leaving MwA (response phase)

请求阶段严格按 MIDDLEWARE 列表里写的顺序(A→B→C)进去,响应阶段严格反过来(C→B→A)出来——写在最前面的中间件,最先碰到请求,也最后碰到响应。

真实源码:洋葱是怎么被拼出来的

拼装过程出人意料地朴素——就是一个 reversed() 循环。

django/core/handlers/base.py · django @ 6.0.7, L26 def load_middleware(self, is_async=False): handler = convert_exception_to_response(get_response) # 最内层:真正处理视图 for middleware_path in reversed(settings.MIDDLEWARE): # 注意:倒着遍历! middleware = import_string(middleware_path) mw_instance = middleware(adapted_handler) # 把"当前 handler"包起来 ... handler = convert_exception_to_response(mw_instance) # 新的 handler 换成刚包好的这层 # 循环结束,handler 就是最外层——从最后一个中间件开始包, # 包到第一个中间件结束,第一个自然就成了最外层

关键在 reversed(settings.MIDDLEWARE):从列表最后一个开始包。第一次循环,C 把"视图本身"包起来,变成新的 handler;第二次循环,B 把"刚包好的 C"包起来;最后一次循环,A 把"已经包了 B、C 的东西"包起来。循环结束后,最外层自然就是列表里写在最前面的那个——这就是"洋葱"的字面构造过程,不是什么特殊算法,就是从后往前一层层套娃。

真实实测:短路之后,外层还会不会收尾

如果中间那层中间件直接返回响应、根本不调用 get_response(),里面的层和视图确实会被跳过——但外层呢?

真实实测
# MIDDLEWARE = [A, Block, C], Block 直接返回响应不调用 get_response
-> entering MwA (request phase) -> entering MwBlock (request phase) !! MwBlock returns EARLY, get_response() never called, inner layers and view SKIPPED <- leaving MwA (response phase) # 注意:MwC 和视图确实完全没跑,但 MwA 的 "leaving" 那行照样打印了

MwC 和视图确实一次都没执行,但 MwA 的响应阶段代码照样跑了——因为短路只是"不往更里层传",并不会让已经在调用栈上的外层代码消失。MwA 在短路发生前已经调用了 get_response(request),这个调用迟早要返回(不管里面发生了什么),返回之后 MwA 自己的收尾代码自然会继续执行。

真实实测:顺序写反了,真实报错

Django 自带的 AuthenticationMiddleware 依赖 SessionMiddleware 先跑——如果顺序反了会怎样?

django/contrib/auth/middleware.py · django @ 6.0.7, L30 class AuthenticationMiddleware(MiddlewareMixin): def process_request(self, request): if not hasattr(request, "session"): raise ImproperlyConfigured( "The Django authentication middleware requires session " "middleware to be installed. Edit your MIDDLEWARE setting to " "insert 'django.contrib.sessions.middleware.SessionMiddleware' " "before 'django.contrib.auth.middleware.AuthenticationMiddleware'." ) request.user = SimpleLazyObject(lambda: get_user(request))
真实实测
# 故意写反顺序:
# MIDDLEWARE = ['...auth.AuthenticationMiddleware', '...sessions.SessionMiddleware']
django.core.exceptions.ImproperlyConfigured: The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE setting to insert 'django.contrib.sessions.middleware. SessionMiddleware' before 'django.contrib.auth.middleware. AuthenticationMiddleware'.

报错信息和源码里的字符串一字不差。原因直接对应上一节的洋葱模型:AuthenticationMiddleware 排在前面 = 更外层 = 请求阶段更早执行,如果它比 SessionMiddleware 先跑,这时候 request.session 还没被设置上,hasattr 检查自然失败。django-admin startproject 生成的默认 MIDDLEWARE 列表里,SessionMiddleware 排第二、AuthenticationMiddleware 排第五,顺序是真的经过设计的,不是随手写的。

顺带一提:request.user = SimpleLazyObject(...) 这行——request.user 本身也是惰性的,和上一篇 QuerySet、上上篇 DeferredAttribute 是同一个思路:真正查用户这件事,被推迟到第一次真的访问 request.user 的时候才做。

演示:洋葱怎么进去,又怎么出来

先看完整走一遍(A→B→C→视图→C→B→A),再看 B 直接短路时会发生什么。

中间件洋葱模型演示
完整通过

数据来自独立验证过的 Python 模拟——用真实的"倒序包装"逻辑复现洋葱构造,两种场景(完整通过、B 短路)都用独立的调用栈回放算法核对过:每一层的"进入"和"离开"事件严格按后进先出配对,短路场景里 C 和视图确实一次都没被记录到。

参考与说明

  • 本文全部真实实验(洋葱执行顺序、短路后外层仍执行响应阶段、中间件顺序写反触发 ImproperlyConfigured)均在本机真实运行的 Django 6.0.7 上完成(用 RequestFactory + WSGIHandler 发真实请求,不是 mock),数据未做删改。
  • 源码引用(django/core/handlers/base.pyBaseHandler.load_middlewaredjango/contrib/auth/middleware.pyAuthenticationMiddleware.process_request)取自本机通过 pip 安装的 Django 6.0.7 包本身,并逐字节比对确认与 django/django 仓库 6.0.7 标签完全一致。默认 MIDDLEWARE 列表(SecurityMiddlewareXFrameOptionsMiddleware 共 7 项)取自 django-admin startproject 使用的真实项目模板文件。
  • 演示动画的洋葱模型(倒序包装构造、短路后的调用栈行为)由 Python 独立实现并交叉验证过——用真实实测过的两个场景(完整通过、B 短路)做参数,每一层的进入/离开事件都用独立的调用栈回放算法核对过严格的后进先出关系。
  • 没有涉及:新式(函数式)中间件和旧式(MiddlewareMixin)中间件在异步场景下的适配细节(adapt_method_mode)、process_view/process_exception/process_template_response 这几个额外钩子的调用时机、MiddlewareNotUsed 异常怎么让某个中间件在运行时被跳过。
☕ 如果这篇文章帮到你,可以请作者喝杯咖啡 · 爱发电