不能只用 @app.exception_handler,因为它仅捕获路由函数内异常,无法处理中间件、依赖注入或 BaseHTTPMiddleware 自身错误;必须用 BaseHTTPMiddleware 子类全局捕获,并按 HTTPException→RequestValidationError→Exception 顺序处理,且须注册为第一个中间件。

为什么不能只用 @app.exception_handler
因为 @app.exception_handler 只能捕获路由函数内部抛出的异常,对中间件里崩掉的代码、依赖注入失败、或 BaseHTTPMiddleware 自身逻辑错误完全无感。比如你在日志中间件里忘了处理 await request.body() 的空请求,直接抛 RuntimeError —— 这个错误根本进不了 @app.exception_handler 的监听范围。
常见现象包括:
- 500 错误响应体为空,或者返回原始 Python traceback(尤其在生产环境未关
debug=True时) - 自定义中间件崩溃后,整个请求链静默中断,前端收不到任何响应
-
RequestValidationError被捕获了,但ValidationError(来自 marshmallow 或 pydantic v1)没被覆盖,仍走默认 422 格式
BaseHTTPMiddleware 中捕获所有异常的写法
必须用 BaseHTTPMiddleware 子类,不能用函数式中间件(它不支持 try/except 包裹整个生命周期)。核心是把 call_next(request) 包进 try,并在 except 里统一构造响应。
关键点:
立即学习“Python免费学习笔记(深入)”;
- 先
except HTTPException,再except RequestValidationError,最后兜底except Exception,顺序错会导致部分异常被吞掉 - 不要在
except Exception里直接raise,否则会跳过后续中间件,且无法返回 JSON - 生产环境务必过滤掉
traceback.format_exception输出,避免泄露路径、变量名等敏感信息
示例片段:
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from pydantic import ValidationError as PydanticValidationError
<p>class GlobalExceptionHandler(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
try:
return await call_next(request)
except HTTPException as e:
return JSONResponse(
status_code=e.status_code,
content={"code": e.status_code, "message": e.detail},
)
except PydanticValidationError as e:
return JSONResponse(
status_code=422,
content={"code": 422, "message": "参数验证失败", "errors": e.errors()},
)
except Exception as e:</p><h1>生产环境建议只记录日志,不返回 traceback</h1><pre class="brush:php;toolbar:false;"> return JSONResponse(
status_code=500,
content={"code": 500, "message": "服务器内部错误"},
)
SkillSub Pro - Python 题解与代码注释双功能技能功能概述SkillSub Pro - Python 题解与代码注释双功能技能是一项面向实际任务的技能,主要用于SkillSub Pro 是一个 Python 题解生成与代码注释的 双功能合体技能 ,专为学生、算法学习者和开发者设计;✅ 一个技能,两种用途 :;核心要点📝 题解模式 :输入题目/题号,自动生成完整 Python 题解(含详细注释、解题思路、复杂度分析);💬 注释模式 :输入 Python 代码,自动添加详细中。它将相关步骤、
注册顺序决定谁能“活到最后”
app.add_middleware() 的调用顺序就是洋葱剥皮顺序:越早注册的中间件,越靠近外层,也就越早拿到请求、越晚拿到响应。全局异常中间件必须注册在最外层(即第一个 add_middleware),否则它捕获不到内层中间件抛的异常。
容易踩的坑:
- 把
CORSMiddleware放在异常中间件前面 → CORS 头还没加,异常响应就发出去了,前端收不到Access-Control-Allow-Origin - 把日志中间件放最前,但异常中间件放后面 → 日志打了一半就崩,没机会记录错误上下文
- 多个异常中间件注册 → 后注册的会覆盖前注册的,
dispatch不会链式调用
正确注册方式(注意位置):
app.add_middleware(GlobalExceptionHandler) # 必须第一行 app.add_middleware(CORSMiddleware, allow_origins=["*"]) app.add_middleware(GZipMiddleware)
和 @app.exception_handler 共存时的冲突点
两者可以共存,但行为有优先级:路由内抛出的异常,先走 @app.exception_handler;中间件抛出的异常,只走 BaseHTTPMiddleware.dispatch。问题在于,如果你在某个中间件里手动 raise HTTPException,它不会触发 @app.exception_handler,除非你显式调用 request.app.router.routes 相关逻辑 —— 这不现实。
所以实际建议:
- 业务逻辑层用
raise HTTPException+@app.exception_handler做语义化处理(如 404 返回带资源名的提示) - 基础设施层(鉴权、限流、日志、DB 连接)的异常,全部交给
BaseHTTPMiddleware统一兜底 - 避免在中间件里 raise 业务异常类(如
UserNotFoundException),否则它们会被except Exception捕获,失去语义
真正难处理的是跨中间件的状态传递 —— 比如你想在异常响应里带上当前请求 ID,就得靠 request.state 在中间件间透传,这个细节很容易被忽略。

















