本系列:00 导读 · 01 心智模型 · 02 路由 · 03 依赖注入 · 04 中间件异常日志(本文) · 05 异步 · 06 鉴权 · 07 测试 · 08 实战
行文:T3 辨析篇 | 本篇方法:交错练习 + 排错 | 全链路心智与 01 §4 同源
0. 完整执行逻辑流程图(任务流转)
读中间件 / 异常 / 日志前,先把一次请求会经过的全部组件摆上台面。细节原理见 01;本节省「在管道哪一层动手」。
0.1 两层时钟(先分清)
| 尺度 | 触发 | 典型组件 |
|---|---|---|
| 进程 / worker | 启动、关闭 | lifespan、连接池、模型加载 |
| 单次 HTTP 请求 | 每个请求一次 | 中间件洋葱 → 路由 → 依赖 → 校验 → 端点 → 响应 →(可选)BackgroundTasks |
lifespan 不插入每个请求中间;WebSocket / mount 子应用是旁路,见 §0.5。
0.2 漫画总览

0.3 组件清单(可能出现的架构件)
| 层 | 组件 | 职责 |
|---|---|---|
| 网络 | 客户端、TCP、反向代理(可选) | 字节进出;TLS/负载均衡常在 uvicorn 外 |
| ASGI 服务器 | uvicorn 等 | 解析 HTTP/WS → 调应用 app(scope, receive, send) |
| 应用壳 | FastAPI / Starlette | 实现 ASGI callable;叠路由与中间件 |
| 横切 | 中间件洋葱(含 CORS 等) | call_next 前/后加工;可短路不进路由 |
| 错误管道 | ExceptionMiddleware 概念层 + exception_handler |
把异常变成 Response |
| 路由 | APIRouter / 路由表 | 按 method+path 匹配;否则 404/405 |
| 注入 | Depends(含安全依赖) | 解析用户、DB session 等 |
| 校验 | Pydantic(Path/Query/Body) | 失败 → 422(可定制 handler) |
| 业务 | 端点 async def / def(后者常进线程池) |
真正业务 |
| 出站整形 | response_model、状态码、Response 子类 |
过滤字段、定成功码 |
| 后置 | BackgroundTasks | 响应发出后同进程轻量活 |
| 协议回写 | send 事件 |
http.response.start / body(或流式多段) |
段末注释:中间件洋葱指请求从外层进、响应从内层出,像剥洋葱;后
add_middleware的一般更靠外。后文「洋葱」即此意。
0.4 HTTP 主路径(精确流转)
1 | flowchart TD |
读图要点:
- 中间件的「前半段」在进路由前;「后半段」在拿到
Response之后——对应await call_next(request)两侧。 - 依赖、校验、端点都在
call_next之内。 HTTPException/ 校验异常通常在进入响应上行前被转成 Response,仍会经过外层中间件后半段(除非中间件自身短路)。
0.5 旁路与变体(完整架构覆盖)
| 变体 | 流转差异 |
|---|---|
| 中间件短路 | 前半段直接 return Response,不调 call_next → 无路由/无依赖 |
| CORS 预检 OPTIONS | 常在最外层 CORSMiddleware 直接应答 |
| WebSocket | scope["type"]=="websocket";长连接收发,不是「一次 Response」模型 |
| StreamingResponse / SSE | 仍走中间件+路由;send 多次 body 块 |
app.mount 子应用 |
前缀匹配后把 scope 交给另一个 ASGI 应用(可另有中间件栈) |
| 多 worker | 每进程独立 lifespan + 独立事件循环;请求粘在某一个 worker |
洋葱顺序放大(请求 ↓ / 响应 ↑):
1 | 请求 ↓ 响应 ↑ |
1. 易混对照表
| 对比 | A | B | 怎么选 |
|---|---|---|---|
| 横切位置 | 中间件 | Depends | 要改每个请求的 request/response 流 → 中间件;要注入业务对象/用户 → Depends |
| 错误出口 | HTTPException |
全局 exception_handler |
业务可预期错误用前者;统一 JSON 形状、未捕获异常用后者 |
| 校验失败 | 422 默认 | 自定义 validation handler | 要友好错误码/字段格式 → 自定义 handler |
| 日志粒度 | 中间件记 access | 业务 logger | access 记路径/耗时;业务记领域事件 |
| 执行顺序 | 中间件 洋葱模型 | 依赖在进路由前 | 中间件包在最外;CORS 通常最外一层 |
段末注释:CORS(Cross-Origin Resource Sharing,跨源资源共享)是浏览器限制下,服务端通过响应头允许前端跨域访问的机制。
2. 中间件组块
1 | import time |
- 洋葱:后
add_middleware的在外层(先收到请求、最后送出响应)。 - 误用:在中间件里做 DB 查询业务——应放 service + Depends。
3. 异常处理组块
3.1 业务可预期:HTTPException
1 | from fastapi import HTTPException, status |
3.2 统一未捕获异常
1 | from fastapi import Request |
3.3 校验错误 422 定制
1 | from fastapi.exceptions import RequestValidationError |
4. 日志组块
1 | import logging |
| 记什么 | 放哪 | 示例字段 |
|---|---|---|
| 每条 HTTP | 中间件 | method, path, status, ms, request_id |
| 业务事件 | service | action, entity_id, outcome |
| 异常栈 | exception handler | logger.exception(...) |
误用:print 调试上线——无级别、无聚合,难与 access 日志关联。
5. 交错练习题(打乱类型)
| # | 场景 | 应选 | 理由 |
|---|---|---|---|
| Q1 | 所有响应加 X-Request-ID |
中间件 | 横切 response |
| Q2 | 从 JWT 解析当前用户 | Depends | 注入路由参数 |
| Q3 | 资源不存在返回 404 JSON | HTTPException | 业务分支 |
| Q4 | 未捕获异常统一 500 形状 | exception_handler | 全局兜底 |
| Q5 | 允许前端 localhost:3000 跨域 |
CORSMiddleware | 浏览器策略 |
| Q6 | Pydantic 校验失败要固定错误格式 | validation handler | 422 定制 |
| Q7 | 记录每个接口耗时 | 中间件 | 与业务解耦 |
| Q8 | 同一 router 要求 API Key | APIRouter(dependencies=...) |
路由级 Depends(见 03) |
6. 康奈尔排错栏(错误 → 现象 → 根因 → 修法)
| 错误用例 | 现象 | 根因 | 修法 |
|---|---|---|---|
中间件里 return 不调 call_next |
所有路由 404/无响应 | 请求未进入应用 | 必须 response = await call_next(request) |
| CORS 加在最内层 | 浏览器仍报 CORS | 预检未过外层 | CORSMiddleware 尽量最先 add(最后注册=最外) |
HTTPException(500) 包 try/except Exception |
客户端收不到预期 body | 被全局 handler 吃掉 | 区分 HTTPException 与 Exception |
| validation handler 返回非 JSON | /docs 调试异常 |
响应类型不对 | 用 JSONResponse |
| 日志无 request_id | 多请求难关联 | 未贯穿中间件 | 中间件写入 header + logger extra |
7. 15 分钟验收骨架
在新 main.py 一次性接入:
1 | # 检查清单(打勾即过关) |
用 curl -i 与故意传错 body 触发 422/500 各一次。
8. 闪卡候选
| 正面 | 背面 |
|---|---|
| 中间件 vs Depends? | 改请求/响应管道 vs 注入参数 |
| 洋葱模型谁先收到请求? | 最后 add_middleware 的最外层 |
| 422 谁抛出? | FastAPI/Pydantic 请求校验 |
生产 500 响应能带 str(exc) 吗? |
不应;记日志,对外通用文案 |
小结
- 先有全链路:中间件包在路由外;Depends/校验/端点在
call_next内;异常多在出洋葱前变成 Response(见 §0)。 - 中间件管管道;Depends管注入;HTTPException管预期错误;handler管兜底与 422 形状。
- 下一篇 05 异步、后台任务与流式:弄清「写了 async 为什么还卡」。