FastAPI 异常处理最佳实践:这套代码模板让你不再 996

在这里插入图片描述

开篇:一个让人抓狂的下午

“接口挂了,返回 500。”

看到这条消息,你打开服务器日志,心想:来吧,看看是什么妖魔鬼怪。

2024-01-15 14:30:00 | INFO | 应用启动完成
2024-01-15 14:30:05 | INFO | 收到请求: POST /api/generate

然后……就没了。

没有错误信息,没有堆栈跟踪,什么都没有。

Bug 像个忍者,来无影去无踪。

你开始加 print,部署,再加 print,再部署……三小时后,终于发现是某个变量是 None。

三个小时,就为了找一个空指针。

罪魁祸首?翻开代码,你看到了这行:

try:
    result = some_function()
except Exception:
    pass  # 就是这行!

某位前人写的代码,捕获了所有异常,然后——什么都不做。

异常就这么被"吞"掉了,悄无声息,像被灭霸打了个响指。

如果你也经历过这种绝望,这篇文章就是为你准备的。


异常处理的"三道防线"

好的异常处理就像洋葱——一层一层的(而且可能让你流泪)。

抛出异常
业务异常
未知异常
HTTP 请求
第一道防线: 全局异常处理器
第二道防线: 业务异常处理
第三道防线: 端点级 try-except
业务逻辑
异常类型?
返回友好错误提示
记录日志 + 返回通用错误

三道防线,各司其职

防线职责比喻
全局异常处理器兜底所有漏网之鱼最后一道城墙
业务异常处理处理"意料之中"的错误前线哨兵
端点级 try-except精细化异常恢复贴身保镖

记住:异常不会消失,只会被藏起来。我们的目标是:让每个异常都无处可藏。


第一步:给异常办个"身份证"

裸奔的 raise Exception("出错了") 是不够的。我们需要给异常分门别类:

# app/core/exceptions.py
from typing import Optional, Any

class AppException(Exception):
    """应用异常基类 —— 所有业务异常的祖宗"""

    def __init__(
        self,
        message: str,
        code: str = "UNKNOWN_ERROR",
        status_code: int = 500,
        details: Optional[Any] = None
    ):
        self.message = message
        self.code = code           # 错误码,前端靠这个判断
        self.status_code = status_code  # HTTP 状态码
        self.details = details     # 额外信息,想塞啥塞啥
        super().__init__(message)


class ValidationError(AppException):
    """参数不对?400 伺候"""
    def __init__(self, message: str, details: Any = None):
        super().__init__(message, "VALIDATION_ERROR", 400, details)


class NotFoundError(AppException):
    """找不到?404 安排"""
    def __init__(self, resource: str, resource_id: str):
        super().__init__(
            f"{resource} not found: {resource_id}",
            "NOT_FOUND",
            404,
            {"resource": resource, "id": resource_id}
        )


class GenerationError(AppException):
    """AI 罢工了"""
    def __init__(self, message: str, model: str = None):
        super().__init__(message, "GENERATION_ERROR", 500, {"model": model})


class ExternalServiceError(AppException):
    """第三方服务挂了,锅不在我"""
    def __init__(self, service: str, message: str):
        super().__init__(
            f"{service} error: {message}",
            "EXTERNAL_SERVICE_ERROR",
            502,  # 502 = 上游挂了
            {"service": service}
        )

有了这套"身份证"系统,每个异常都有:

  • 错误码:前端可以根据 code 显示不同的提示
  • 状态码:HTTP 语义正确,运维监控不会瞎报警
  • 详情:调试时的救命稻草

第二步:设立"全局关卡"

接下来,在 FastAPI 里注册异常处理器。把它想象成机场安检——每个异常都得过这一道:

# app/main.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from app.core.exceptions import AppException
from app.core.logger import logger
import traceback

app = FastAPI()

@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
    """业务异常处理器 —— 处理"可预期的坏消息" """

    logger.warning(
        f"业务异常 [{exc.code}]: {exc.message}",
        extra={
            "path": request.url.path,
            "method": request.method,
            "code": exc.code,
            "details": exc.details
        }
    )

    return JSONResponse(
        status_code=exc.status_code,
        content={
            "success": False,
            "error": {
                "code": exc.code,
                "message": exc.message,
                "details": exc.details
            }
        }
    )


@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    """全局异常处理器 —— 最后一道城墙,专治各种"意外惊喜" """

    error_detail = {
        "code": "INTERNAL_ERROR",
        "message": "An internal error occurred. Please try again later."
    }

    # 开发环境?把底裤都给你看
    if settings.DEBUG:
        error_detail["message"] = str(exc)
        error_detail["type"] = type(exc).__name__
        error_detail["traceback"] = traceback.format_exc().split("\n")

    # 不管什么环境,日志里必须有完整信息
    logger.error(
        f"未处理的异常: {type(exc).__name__}: {str(exc)}",
        extra={
            "path": request.url.path,
            "method": request.method,
            "traceback": traceback.format_exc()  # 完整堆栈,一个字都不能少!
        }
    )

    return JSONResponse(
        status_code=500,
        content={"success": False, "error": error_detail}
    )

划重点

  • AppExceptionWARNING 级别——这是"意料之中"的错误
  • 未知 ExceptionERROR 级别——这是"意料之外"的惊喜
  • traceback.format_exc() 是你的好朋友,完整堆栈一览无余
  • 生产环境别暴露内部错误,不然黑客会感谢你的坦诚

第三步:端点级"精细作战"

全局处理器是最后防线,但有些异常需要在端点级别就地解决:

# app/api/endpoints/generate.py

@router.post("/shot-image")
async def generate_shot_image(request: GenerateShotImageRequest):
    """生成分镜图片 —— 一个充满意外的端点"""

    # 参数校验:先礼后兵
    if not request.prompt.strip():
        raise ValidationError("Prompt cannot be empty")

    try:
        adapter = ImageGenerationAdapterFactory.get_current_adapter()
        result = await adapter.generate_shot_image(
            prompt=request.prompt,
            width=request.width,
            height=request.height
        )

        if not result.get("success"):
            raise GenerationError(
                result.get("error", "Unknown generation error"),
                model=settings.IMAGE_MODEL
            )

        return {"success": True, "data": result}

    except GenerationError:
        raise  # 已经是业务异常,放它走

    except torch.cuda.OutOfMemoryError:
        # 显存爆了?给个人话提示
        raise GenerationError(
            "GPU out of memory. Please try a smaller image size."
        )

    except Exception as e:
        # 未知异常:先记录,再转换
        logger.error(f"Unexpected error in generate_shot_image: {e}")
        raise GenerationError(f"Generation failed: {str(e)}")

异常处理策略表

情况处理方式理由
已是业务异常直接 raise已经有身份证了
已知特定异常转换为业务异常给它办个身份证
未知异常记日志 + 转换先留案底,再处理

第四步:日志配置——"案发现场"的监控探头

推荐 loguru,比标准库的 logging 好用一万倍(真的):

# app/core/logger.py
from loguru import logger
import sys

logger.remove()  # 先清场

# 控制台输出:花里胡哨但好用
logger.add(
    sys.stdout,
    format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
           "<level>{level: <8}</level> | "
           "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
           "<level>{message}</level>",
    level="DEBUG"
)

# 文件日志:朴实无华但可靠
logger.add(
    "logs/app_{time:YYYY-MM-DD}.log",
    format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}",
    level="INFO",
    rotation="00:00",      # 每天零点轮转
    retention="30 days",   # 保留 30 天
    compression="zip"      # 自动压缩,省空间
)

# 错误日志:单独伺候,VIP 待遇
logger.add(
    "logs/error_{time:YYYY-MM-DD}.log",
    format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}\n{exception}",
    level="ERROR",
    rotation="00:00",
    retention="90 days"   # 错误日志多留几天,翻旧账用得上
)

第五步:请求追踪——给每个请求一个"案件编号"

import time
import uuid

@app.middleware("http")
async def log_requests(request: Request, call_next):
    """请求日志中间件 —— 来了就得登记"""

    request_id = str(uuid.uuid4())[:8]  # 8 位够用了

    logger.info(f"[{request_id}] --> {request.method} {request.url.path}")

    start = time.time()
    response = await call_next(request)
    duration = time.time() - start

    logger.info(f"[{request_id}] <-- {response.status_code} in {duration:.2f}s")

    # 响应头里也带上,方便前端
    response.headers["X-Request-ID"] = request_id

    return response

效果展示

2024-01-15 14:30:00 | INFO | [a1b2c3d4] --> POST /api/generate/shot-image
2024-01-15 14:30:12 | INFO | [a1b2c3d4] <-- 200 in 12.34s

前端说"接口报错了",你只需要问一句:“Request ID 多少?”

然后 grep a1b2c3d4 logs/error_*.log,破案。


红线:绝对禁止的写法

立个规矩,刻在 DNA 里:

# 禁止!禁止!禁止!
try:
    something()
except Exception:
    pass  # 这是在犯罪

这种代码的危害:

  1. 异常凭空消失,debug 时怀疑人生
  2. 埋下定时炸弹,不知道什么时候爆炸
  3. 让接手的同事想打人

正确姿势

# 姿势一:只捕获特定异常
try:
    result = maybe_fail()
except FileNotFoundError:
    result = default_value  # 这个异常我能处理
except PermissionError as e:
    logger.warning(f"权限不足: {e}")
    raise  # 这个我处理不了,抛出去

# 姿势二:实在要忽略,至少留个遗言
try:
    optional_operation()
except SomeSpecificError as e:
    logger.debug(f"忽略的异常(有意为之): {e}")

快速抄作业清单

原则做法
不吞异常except: pass 写一次,绩效扣一分
分层处理全局 → 业务 → 端点,层层设防
日志完整堆栈、请求 ID、路径,一个都不能少
错误码标准化定义业务异常类,别裸抛 Exception
环境区分开发给详情,生产给脸色
请求可追踪X-Request-ID,追凶利器

总结:异常处理三原则

把这三句话贴在工位上:

  1. 要么处理它——你知道怎么应对这个异常
  2. 要么记录它——你不知道怎么处理,但要留下证据
  3. 要么抛出它——让更上层的人来处理

但绝不能忽视它。

有了这套体系,下次接口报 500,你只需要:

  1. 拿到 Request ID
  2. 搜索错误日志
  3. 看完整堆栈
  4. 定位问题

整个过程,一分钟

再也不用三小时排查一个空指针了。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员义拉冠

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值