错误处理与中间件:接口不能 500 裸奔
统一错误响应、异常兜底中间件、4xx 与 5xx 的语义分工
- 理解统一错误响应体长什么样
- 掌握中间件链「记录耗时 → 异常兜底」的洋葱模型
- 能区分 4xx(客户端错)与 5xx(服务端错)
- 建立「try 住所有意外并返回友好 JSON」的接口惯例
别把一锅没做好的菜直接端回厨房甩给客人,要给客人一张「换券说明」:哪里出了问题、下一步怎么办。接口也一样——崩了就吐一串堆栈给前端,等于把锅甩给用户;正确做法是返回一份格式统一、人话写的错误 JSON。
为什么不能 500 裸奔?
一个接口抛了未捕获异常,框架默认返回 500 + 一大段堆栈。前端拿到这种东西根本没法提示用户,只能傻转圈。生产环境把堆栈直接暴露给外人,还是信息泄露——攻击者能从堆栈里看出你用了什么框架、哪张表、哪个函数,等于把底牌亮给对手。
正确姿势是全接口统一错误体,例如:{ "error": "服务器开小差了,请稍后再试", "code": 500 }。前端只认 code 和 error 两个字段,不管后端内部炸成什么样,提示语都长一个样。
4xx:请求本身有问题,锅在客户端。400 参数错、401 没登录、403 没权限、404 不存在。5xx:服务端自己崩了,锅在后端。500 内部错误、502 网关错、503 服务暂时不可用。判断口诀:参数不对报 4xx,代码自己炸了报 5xx。
业务码 vs HTTP 状态码:两套编号别混
新手最常搞混的是「HTTP 状态码」和「业务错误码」。HTTP 状态码是协议规定的,表达这次请求在传输层成没成:200 通了、404 路不存在、500 服务器炸了。业务错误码是你自己在 body 里定的枚举,表达业务上哪一步错了,比如 10001 用户不存在、10002 余额不足、10003 优惠券已过期。
两种极端都别踩。一是把所有业务错都报成 HTTP 500:用户填错密码也算服务器故障,监控会被正常的输入错误告警刷屏,真故障反而被淹没。二是永远返回 HTTP 200 靠 body 里的业务码区分:这样 CDN、网关、日志系统无法按状态码做缓存和限流,运维盲人摸象。比较稳的做法是——请求合法但业务失败用 200 或 400 配业务码,真异常才动 5xx。
HTTP 码(协议层):200 成功、400 参数错、401 未登录、403 无权、404 不存在、500 服务器错。业务码(应用层):10001 用户不存在、10002 余额不足、10003 优惠券过期……自定义枚举。口诀:HTTP 码告诉「请求通不通」,业务码告诉「事办成没办成」。
中间件链:记录耗时 + 异常兜底
中间件(Middleware)是夹在「请求进来」和「业务处理」之间的一层。它能做两件最常用的事:给每个请求记日志和耗时,把没接住的异常兜成统一错误体。下面用纯 Python 模拟这条链。
洋葱模型:为什么异常中间件要放最外层
中间件的执行顺序像剥洋葱:请求从外层一层层往里钻,业务处理在最里层;响应再从里一层层往外走。把 error_middleware 放在最外层,它才能兜住里面所有层抛出的异常。上面代码里 dispatch 把 error 包在 timing 外面、timing 包在 app 外面,就是这个道理——最外面那层是唯一能看到全部异常的位置。
常见误区是在每个业务函数里到处 try/except,然后各自拼错误 JSON,导致同一个项目里错误体长十几种样。正确做法是把兜底抽到最外层中间件,业务代码只负责「出事就抛」,根本不用管最后怎么返回给前端。
参数校验:Pydantic 的思路
FastAPI 里请求体用 Pydantic 模型声明,字段类型不对框架自动返回 422 校验错误,不用自己写 if 判断。核心思路:把「这个请求合不合法」写成类型声明,而不是一堆手写 if。校验失败属于客户端错,所以是 4xx(422 Unprocessable Entity),不是 500。
# Pydantic 思路(静态示意,真实环境由 FastAPI 自动校验)
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
name: str # 必须是字符串
age: int = 0 # 必须是整数,默认 0
email: EmailStr # 必须长得像邮箱
# 请求体少了 name、age 传了 "abc",框架直接返回 422,
# 业务代码永远收到已经校验好的数据进程级兜底与日志红线
就算路由层兜得再好,总有些异步任务、定时器、数据库断连回调里抛的异常漏网。Node 里要监听 process.on('uncaughtException') 和 process.on('unhandledRejection'),Python 里用 sys.excepthook 或全局钩子,把这种漏网异常记下来再决定是重启还是继续。一个没接住的 Promise rejection 在生产上能直接把进程搞崩,而监控还没反应过来。
日志是排查问题的眼睛,但下面这些字段写进日志等于泄密:用户密码、JWT token、身份证号、银行卡号、完整 Cookie。为什么:日志会被采集到日志系统和云监控,看日志的人远多于能直接查库的人;一旦把 token 打进去,任何看日志的人都能冒充该用户。正确做法:密码直接不打,token 只打前 6 位便于排查,身份证、手机号中间打星号脱敏。
这一层只做「统一格式 + 兜底 + 校验」。十二要素应用(12-Factor)、监控告警、链路追踪这些是上线后才需要的,归到 s15-m7-l1 讲,s7 不展开。
前端提交注册时漏传了必填的 email 字段,后端应该返回哪种状态码?
用户在登录接口把密码填错了,业务码返回 10001。下面哪种处理最合理?
资深工程师加餐
底层原理 · 大厂视角 · 工程经验,点卡片展开
网络侧:DNS→TCP/TLS→HTTP 请求与响应;浏览器侧:解析 HTML 建 DOM、解析 CSS 建 CSSOM、合并成渲染树,再经过布局(Layout)、绘制(Paint)、合成(Composite)上屏。前端性能优化几乎都围绕这条链路:减小资源体积、优化关键渲染路径、避免频繁触发重排重绘。
挑战任务
状态码分类器
场景:你的后端把所有异常都直接抛给前端,前端只会傻转圈。先写一个 classify(status) 把状态码归类,前端据此决定提示语——2xx 归 ok、4xx 归 client、5xx 归 server,其余归 unknown。
课后作业
设计你的错误体
场景:你接手一个老项目,接口崩了直接吐堆栈。请写清三件事——①你的统一错误 JSON 长什么样(至少 code、message、traceId 三个字段);②业务码怎么规划段位(哪段代表用户错、哪段代表服务错);③哪三类信息永远不许进日志。