
开篇:一个让人抓狂的下午
“接口挂了,返回 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 # 就是这行!
某位前人写的代码,捕获了所有异常,然后——什么都不做。
异常就这么被"吞"掉了,悄无声息,像被灭霸打了个响指。
如果你也经历过这种绝望,这篇文章就是为你准备的。
异常处理的"三道防线"
好的异常处理就像洋葱——一层一层的(而且可能让你流泪)。
三道防线,各司其职:
| 防线 | 职责 | 比喻 |
|---|---|---|
| 全局异常处理器 | 兜底所有漏网之鱼 | 最后一道城墙 |
| 业务异常处理 | 处理"意料之中"的错误 | 前线哨兵 |
| 端点级 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}
)
划重点:
AppException用WARNING级别——这是"意料之中"的错误- 未知
Exception用ERROR级别——这是"意料之外"的惊喜 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 # 这是在犯罪
这种代码的危害:
- 异常凭空消失,debug 时怀疑人生
- 埋下定时炸弹,不知道什么时候爆炸
- 让接手的同事想打人
正确姿势:
# 姿势一:只捕获特定异常
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,追凶利器 |
总结:异常处理三原则
把这三句话贴在工位上:
- 要么处理它——你知道怎么应对这个异常
- 要么记录它——你不知道怎么处理,但要留下证据
- 要么抛出它——让更上层的人来处理
但绝不能忽视它。
有了这套体系,下次接口报 500,你只需要:
- 拿到 Request ID
- 搜索错误日志
- 看完整堆栈
- 定位问题
整个过程,一分钟。
再也不用三小时排查一个空指针了。

1599

被折叠的 条评论
为什么被折叠?



