news 2026/4/22 19:41:18

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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()exceptException:pass# 就是这行!

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

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

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


异常处理的"三道防线"

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

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

三道防线,各司其职

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

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


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

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

# app/core/exceptions.pyfromtypingimportOptional,AnyclassAppException(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)classValidationError(AppException):"""参数不对?400 伺候"""def__init__(self,message:str,details:Any=None):super().__init__(message,"VALIDATION_ERROR",400,details)classNotFoundError(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})classGenerationError(AppException):"""AI 罢工了"""def__init__(self,message:str,model:str=None):super().__init__(message,"GENERATION_ERROR",500,{"model":model})classExternalServiceError(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.pyfromfastapiimportFastAPI,Requestfromfastapi.responsesimportJSONResponsefromapp.core.exceptionsimportAppExceptionfromapp.core.loggerimportloggerimporttraceback app=FastAPI()@app.exception_handler(AppException)asyncdefapp_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})returnJSONResponse(status_code=exc.status_code,content={"success":False,"error":{"code":exc.code,"message":exc.message,"details":exc.details}})@app.exception_handler(Exception)asyncdefglobal_exception_handler(request:Request,exc:Exception):"""全局异常处理器 —— 最后一道城墙,专治各种"意外惊喜" """error_detail={"code":"INTERNAL_ERROR","message":"An internal error occurred. Please try again later."}# 开发环境?把底裤都给你看ifsettings.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()# 完整堆栈,一个字都不能少!})returnJSONResponse(status_code=500,content={"success":False,"error":error_detail})

划重点

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

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

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

# app/api/endpoints/generate.py@router.post("/shot-image")asyncdefgenerate_shot_image(request:GenerateShotImageRequest):"""生成分镜图片 —— 一个充满意外的端点"""# 参数校验:先礼后兵ifnotrequest.prompt.strip():raiseValidationError("Prompt cannot be empty")try:adapter=ImageGenerationAdapterFactory.get_current_adapter()result=awaitadapter.generate_shot_image(prompt=request.prompt,width=request.width,height=request.height)ifnotresult.get("success"):raiseGenerationError(result.get("error","Unknown generation error"),model=settings.IMAGE_MODEL)return{"success":True,"data":result}exceptGenerationError:raise# 已经是业务异常,放它走excepttorch.cuda.OutOfMemoryError:# 显存爆了?给个人话提示raiseGenerationError("GPU out of memory. Please try a smaller image size.")exceptExceptionase:# 未知异常:先记录,再转换logger.error(f"Unexpected error in generate_shot_image:{e}")raiseGenerationError(f"Generation failed:{str(e)}")

异常处理策略表

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

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

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

# app/core/logger.pyfromloguruimportloggerimportsys 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"# 错误日志多留几天,翻旧账用得上)

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

importtimeimportuuid@app.middleware("http")asyncdeflog_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=awaitcall_next(request)duration=time.time()-start logger.info(f"[{request_id}] <--{response.status_code}in{duration:.2f}s")# 响应头里也带上,方便前端response.headers["X-Request-ID"]=request_idreturnresponse

效果展示

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()exceptException:pass# 这是在犯罪

这种代码的危害:

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

正确姿势

# 姿势一:只捕获特定异常try:result=maybe_fail()exceptFileNotFoundError:result=default_value# 这个异常我能处理exceptPermissionErrorase:logger.warning(f"权限不足:{e}")raise# 这个我处理不了,抛出去# 姿势二:实在要忽略,至少留个遗言try:optional_operation()exceptSomeSpecificErrorase:logger.debug(f"忽略的异常(有意为之):{e}")

快速抄作业清单

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

总结:异常处理三原则

把这三句话贴在工位上:

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

但绝不能忽视它。

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

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

整个过程,一分钟

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/20 16:43:39

Percona XtraBackup入门指南:从安装到第一个备份

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个交互式Percona XtraBackup学习环境&#xff0c;包含&#xff1a;1. 分步安装向导 2. 可视化命令生成器(选择备份类型、压缩选项等) 3. 模拟备份/恢复过程动画演示 4. 常见错…

作者头像 李华
网站建设 2026/4/18 9:50:38

5G通信中的锁相环实战应用解析

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个5G基站锁相环系统仿真演示&#xff0c;展示锁相环在载波同步和时钟恢复中的应用。要求&#xff1a;1. 模拟5G NR信号环境 2. 实现数字锁相环算法 3. 展示相位跟踪过程 4. 测…

作者头像 李华
网站建设 2026/4/22 9:55:24

ModHeader插件在爬虫开发中的5个实战技巧

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个爬虫调试助手工具&#xff0c;集成ModHeader核心功能并扩展&#xff1a;1.预置常见爬虫请求头配置(Googlebot/Baiduspider等) 2.支持请求头随机生成器避免被封禁 3.添加自动…

作者头像 李华
网站建设 2026/4/11 14:32:06

java 中四种引用类型介绍

在java中&#xff0c;对象的引用强度被分为四种&#xff0c;从强到弱一次是&#xff1a;强引用 → 软引用 → 弱引用 → 虚引用 它们都位于 java.lang.ref 包中&#xff0c;主要用于内存管理、缓存设计、避免 OOM 等场景 一、强引用&#xff08;Strong Reference&#xff09; 1…

作者头像 李华
网站建设 2026/4/22 18:38:48

aTeX 学习笔记:学术文档排版

在实际应用中&#xff0c;如果我们仅仅需要完成的是《[[LaTeX学习笔记&#xff1a;文档排版基础]]》中所介绍的那些纯文本排版工作&#xff0c;其实并不一定需要用到 LATEX这样复杂的排版系统。毕竟&#xff0c;LATEX的核心优势主要在于其对数学公式、图表、参考文献等复杂文档…

作者头像 李华
网站建设 2026/4/21 18:12:17

零基础教程:5分钟用AI创建你的第一个抖音录播工具

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个最简单的抖音直播录制工具demo&#xff0c;要求&#xff1a;1.极简实现(不超过200行代码) 2.只需核心录制功能 3.提供最基础的命令行界面 4.包含最简单的错误提示 5.有清晰…

作者头像 李华