
阅读大约需 10 分钟
那个神秘的 500 错误
周三下午,前端同事发来消息:“小禾,生成图片的接口挂了,返回 500。”
小禾打开服务器日志:
2024-01-15 14:30:00 | INFO | 应用启动完成
2024-01-15 14:30:05 | INFO | 收到请求: POST /api/generate
...
然后就没了。
没有错误信息,没有堆栈跟踪,什么都没有。
小禾本地跑了一遍,正常。
他又看了看前端控制台:
POST /api/generate 500 (Internal Server Error)
{"detail": "Internal Server Error"}
就这么一行,没有任何有用的信息。
小禾开始漫长的排查之旅:
14:35 - 加了几个 print,重新部署
14:50 - 发现 print 没输出,可能没走到那
15:20 - 又加了几个 print,又部署
15:45 - 终于发现是某个深层函数抛了异常
16:00 - 定位到问题:一个变量是 None
16:10 - 修复,部署,测试通过
三个小时,就为了找一个空指针。
小禾越想越气:为什么异常没打日志?
他翻了翻代码,找到了罪魁祸首:
try:
result = some_function()
except Exception:
pass # 就是这行!
某位前人写的代码,捕获了所有异常,然后什么都不做。
异常就这么被吞掉了,悄无声息。
异常处理的三个层级
痛定思痛,小禾决定重构整个异常处理体系。
他画了张架构图:
三层防护,各司其职:
- 全局异常处理器:兜底所有未处理的异常,记录日志,返回统一格式
- 业务异常处理:处理可预期的业务错误,返回友好提示
- 端点级 try-except:处理特定接口的特定异常,做细粒度恢复
定义业务异常类
首先,定义一套业务异常类:
# 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
self.details = details
super().__init__(message)
class ValidationError(AppException):
"""数据验证错误"""
def __init__(self, message: str, details: Any = None):
super().__init__(message, "VALIDATION_ERROR", 400, details)
class NotFoundError(AppException):
"""资源不存在"""
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,
{"service": service}
)
有了这套异常类,错误就有了明确的分类和结构。
全局异常处理器
然后,在 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):
"""处理业务异常"""
# 业务异常用 warning 级别
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 级别,记录完整堆栈
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": {
"code": "INTERNAL_ERROR",
"message": "An internal error occurred. Please try again later."
}
}
)
关键点:traceback.format_exc() 会记录完整的堆栈信息。
有了这个,再也不用加 print 去猜异常在哪了。
端点级异常处理
对于特定接口,可以做更细粒度的处理:
# app/api/endpoints/generate.py
from app.core.exceptions import GenerationError, ValidationError
@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)}")
这样:
- 参数错误返回 400
- 显存不足返回 500,但有友好提示
- 其他异常也会被捕获,不会"消失"
日志配置
小禾选用了 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" # 压缩旧日志
)
# 错误日志单独文件
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):
"""记录请求日志"""
# 生成请求 ID
request_id = str(uuid.uuid4())[: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
有了请求 ID,前端报错时只要把 ID 发过来,就能快速定位日志。
开发环境返回详细错误
生产环境不能暴露内部错误,但开发环境可以:
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""处理所有未捕获的异常"""
error_detail = {
"code": "INTERNAL_ERROR",
"message": str(exc)
}
# 开发环境返回堆栈
if settings.DEBUG:
error_detail["type"] = type(exc).__name__
error_detail["traceback"] = traceback.format_exc().split("\n")
logger.error(
f"未处理的异常: {type(exc).__name__}: {str(exc)}",
extra={"traceback": traceback.format_exc()}
)
return JSONResponse(
status_code=500,
content={"success": False, "error": error_detail}
)
开发环境的响应:
{
"success": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "'NoneType' object has no attribute 'prompt'",
"type": "AttributeError",
"traceback": [
"Traceback (most recent call last):",
" File \"app/api/endpoints/generate.py\", line 42, in generate_shot_image",
" prompt = request.data.prompt",
"AttributeError: 'NoneType' object has no attribute 'prompt'"
]
}
}
一眼就能看出问题在哪。
禁止吞异常
最后,小禾在团队立下一条规矩:
禁止写 except: pass
如果真的要忽略某个异常,至少打个日志:
# 错误写法
try:
something()
except Exception:
pass # 禁止!
# 正确写法
try:
something()
except SomeSpecificError as e:
logger.debug(f"忽略的异常: {e}") # 至少留个记录
更好的做法是只捕获特定异常:
# 最佳实践
try:
result = maybe_fail()
except FileNotFoundError:
result = default_value
except PermissionError as e:
logger.warning(f"权限不足: {e}")
raise
# 其他异常会自动向上抛出
异常处理清单
小禾总结了一份清单:
| 原则 | 实践 |
|---|---|
| 不要吞异常 | except: pass 是禁止的 |
| 分层处理 | 全局 -> 业务 -> 端点 |
| 日志要完整 | 包含堆栈、请求信息 |
| 错误码标准化 | 定义业务异常枚举 |
| 开发/生产区分 | 开发环境暴露详情 |
| 请求可追踪 | 添加 request_id |
小禾的感悟
那三小时的排查,
让我学会一个道理:
异常不会消失,
只会被藏起来。
except pass 是懒惰,
是给未来的自己挖坑。
每个异常都有它的意义,
要么处理它,
要么记录它,
要么抛出它。
但绝不能忽视它。
全局处理器是保险,
业务异常是分类,
日志是证据。
有了这三样,
再也不用三小时找 bug 了。
下次有人说 500,
我只需要一分钟。
小禾看着清晰的日志输出,心情大好。
三小时的教训,换来了一套完善的异常处理体系。
值了。
下一篇预告:前端传了个 null,后端直接炸了
防御性编程,让你的接口固若金汤。
敬请期待。

5851

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



