FastAPI路由配置:如何结合中间件与全局异常处理实现统一响应封装
你在ClientIPMiddleware中编写的try/except无法捕获任何错误,核心原因是Starlette/FastAPI的请求处理链路中,异常拦截层位于BaseHTTPMiddleware的内层:所有端点抛出的异常会先被框架已注册的异常处理器捕获、生成对应的响应对象后,再传回给call_next,整个过程不会向外层中间件抛出原始异常,因此外层的except块永远无法触发。
另外你原有HTTPException处理器中引用的exc.status属性在Starlette的HTTPException类中不存在,实际类属性为status_code,原逻辑运行时会触发属性错误,需在实现时修正。
你不需要额外编写自定义装饰器,FastAPI原生的@app.exception_handler本身就是全局生效的装饰器,注册后会自动捕获所有路由端点抛出的对应类型异常。直接基于原生的异常注册机制+中间件分层即可同时满足两个核心能力:
- 覆盖所有层级的异常(包括HTTP类异常、未捕获的通用运行时异常),统一返回带
status、detail字段的JSON格式响应 - 中间件仅负责请求上下文注入、成功响应的统一包装,不承担异常捕获职责
设计上不推荐将异常捕获逻辑放入
BaseHTTPMiddleware中,一方面框架的异常拦截层本身就处于中间件内层,外层try无法拿到原始异常;另一方面异常处理属于全局横切逻辑,使用原生异常注册机制实现的可维护性、可扩展性远高于在中间件中硬编码捕获逻辑。
具体实现步骤
- 补全全局异常处理器,覆盖所有可能抛出的异常类型,保证响应格式统一
- 修正原有
HTTPException处理器的属性读取错误,保证响应结构一致 - 增加通用
Exception类的兜底处理器,捕获所有未被其他逻辑处理的运行时错误,避免框架默认返回不符合格式要求的500响应
- 修正原有
- 重构
ClientIPMiddleware,移除无效的try/except块,仅保留原有客户端IP注入逻辑、以及非GET请求200状态的响应包装逻辑 - 按照链路顺序注册中间件,保证上下文注入的时机正确
完整可运行代码
import typing import fastapi import starlette.exceptions import starlette.middleware.base app = fastapi.FastAPI(openapi_url='/api/v1/openapi.json') app.include_router(v1.api_router, prefix='/api/v1') # 全局HTTP异常处理器:统一响应格式 @app.exception_handler(starlette.exceptions.HTTPException) async def on_http_exception_handler( request: fastapi.Request, exc: starlette.exceptions.HTTPException, ): return fastapi.responses.JSONResponse( status_code=exc.status_code, content={ 'detail': exc.detail, 'status': exc.status_code, }, ) # 兜底通用异常处理器:捕获所有未处理的运行时错误 @app.exception_handler(Exception) async def on_uncaught_exception_handler( request: fastapi.Request, exc: Exception, ): # 生产环境可在此处补充异常堆栈日志记录 return fastapi.responses.JSONResponse( status_code=500, content={ 'detail': '服务器内部错误', 'status': 500, }, ) # 重构后的客户端IP中间件 class ClientIPMiddleware(starlette.middleware.base.BaseHTTPMiddleware): async def dispatch(self, request: fastapi.Request, call_next: typing.Callable): # 原有逻辑:提取客户端IP注入请求上下文 request.state.client_ip = get_client_ip(request.headers) response = await call_next(request) # 拟新增逻辑:非GET请求返回200时,统一返回成功格式 if request.method != 'GET' and response.status_code == 200: return fastapi.responses.JSONResponse( status_code=200, content={'status': 'ok'}, ) return response # 注册中间件 app.add_middleware(ClientIPMiddleware)
注意:中间件的执行顺序遵循“后注册、先执行”的规则,当前将
ClientIPMiddleware放在路由挂载完成后注册,会让它处于最外层的请求链路位置,既可以正常给请求对象注入上下文,也能拿到经过异常处理器处理后的响应对象做统一包装。
内容的提问来源于stack exchange,提问作者yose93

