You need to enable JavaScript to run this app. FastAPI路由配置:如何结合中间件与全局异常处理实现统一响应封装
优惠活动
大模型
产品
解决方案
定价
更多

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无法拿到原始异常;另一方面异常处理属于全局横切逻辑,使用原生异常注册机制实现的可维护性、可扩展性远高于在中间件中硬编码捕获逻辑。

具体实现步骤

  1. 补全全局异常处理器,覆盖所有可能抛出的异常类型,保证响应格式统一
    • 修正原有HTTPException处理器的属性读取错误,保证响应结构一致
    • 增加通用Exception类的兜底处理器,捕获所有未被其他逻辑处理的运行时错误,避免框架默认返回不符合格式要求的500响应
  2. 重构ClientIPMiddleware,移除无效的try/except块,仅保留原有客户端IP注入逻辑、以及非GET请求200状态的响应包装逻辑
  3. 按照链路顺序注册中间件,保证上下文注入的时机正确

完整可运行代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.30 19:36:04