starlette中间件必须是接受app、scope、receive、send四参数的异步函数,且顺序不可错;需用工厂模式支持配置;修改请求/响应须流式处理并确保send调用完整;异常处理须先send再raise;调试宜用print(scope["path"])定位。

Starlette 中间件必须是 callable,且参数顺序不能错
Starlette 要求中间件必须是接受 app 和 scope、receive、send 三参数的异步函数(或返回该函数的工厂),否则直接报 TypeError: middleware must be a callable 或运行时 RuntimeError: Expected scope to be a dict。
常见错误是写成同步函数、漏掉 scope、或把 receive/send 放在错误位置。正确签名只有一种:
async def my_middleware(app, scope, receive, send):
如果需要配置参数(比如日志前缀),必须用工厂模式:
def make_logging_middleware(prefix="REQ"):
async def middleware(app, scope, receive, send):
# ...
return middleware- 不能写成
async def middleware(app, receive, send, scope)—— Starlette 按固定顺序解包 - 不能省略
scope:它是 ASGI 生命周期的上下文入口,含type、path、headers等关键字段 - 若在中间件里调用
await receive(),必须确保后续也调用await send(...),否则连接挂起
修改请求/响应体需用 StreamingResponse 或重写 body 字段
Starlette 中间件无法像 Flask 那样直接改 request.body 或 response.body —— 因为 ASGI 协议中 body 是通过 receive 流式接收、send 流式发送的。想篡改内容,得自己拼接完整 body 再构造新响应。
立即学习“Python免费学习笔记(深入)”;
典型场景:统一添加响应头、记录原始请求体、过滤敏感字段。错误做法是试图给 scope["body"] 赋值(它根本不存在);正确路径是拦截 receive、缓存数据,再用 StreamingResponse 或 Response 重发。
- 读取完整 body:需循环
await receive()直到message["type"] == "http.request"且"more_body": False - 若只读不改,记得把收到的每条 message 原样传给下游
await send(message),否则请求中断 - 性能敏感场景慎用:读取全部 body 会阻塞整个流,大文件上传时可能 OOM;建议按需判断
scope["method"]和scope["path"]再决定是否缓冲
异常处理必须 await send() 后再 raise,否则 500 页面不显示
在中间件里捕获异常后,如果直接 raise,Starlette 默认异常处理器可能收不到完整 scope,导致返回空白响应或 500 但无 traceback。必须手动调用 send() 发送状态行和 headers,再抛出。
例如记录错误并返回 JSON 错误页:
try:
await self.app(scope, receive, send)
except Exception as exc:
await send({
"type": "http.response.start",
"status": 500,
"headers": [(b"content-type", b"application/json")],
})
await send({
"type": "http.response.body",
"body": b'{"error": "Internal server error"}',
"more_body": False,
})
raise- 漏掉
await send(...)就等于没发 HTTP 响应头,客户端一直等待 -
raise必须在send之后,否则异常中断执行流,send 不会被执行 - 不要在中间件里吞掉异常(如只 log 不 raise),否则上层异常处理器失效,debug 成本陡增
调试中间件顺序:用 print + scope["path"] 快速定位执行点
Starlette 的中间件是嵌套调用的,外层中间件的 send 实际是内层中间件的 send,容易搞不清哪一层在处理哪个请求。最轻量的调试方式是在每个中间件开头加一行 print(f"[{name}] {scope['path']}")。
注意 scope["path"] 是原始路径(未被路由解析),而 scope.get("route") 通常为空——因为路由匹配发生在中间件之后。所以不能靠 route 判断是否命中某 endpoint。
- 多个中间件共用同一份
scope字典,但它是只读的;修改scope["path"]不会影响后续路由(Starlette 路由器用的是副本) - 测试时用
curl -v http://localhost:8000/api/v1/users比看日志更直观:能立刻看到哪些中间件被触发、顺序是否符合预期 - 生产环境禁用 print;替换为 structlog 或 logging.getLogger(__name__).debug(),且确保 level >= DEBUG
中间件不是万能胶,它看不到路由参数、拿不到依赖注入后的 request 对象,所有“业务逻辑级”操作(比如验证 JWT、获取当前用户)更适合放在 route handler 或依赖项里。强行塞进中间件,只会让 scope 变脏、调试变难、复用性归零。










