
上一章收尾时埋了个钩子:给每次请求加 request_id。落地之后,正常响应、模型调用失败、API Key 缺失,都已经带上了同一个号。前端一报错,把号甩给后端,就能去日志里捞出同一次请求的全过程。
但前两天联调,我故意传了段超短简历,想看看参数校验报错长什么样。返回的 422 里,request_id 没了。
这就有意思了。同一个服务,为什么校验错误偏偏不带号?这一篇就把这个漏洞补上。
它不是用户 ID,也不是业务 ID,是用来标识「这一次 HTTP 请求」的。
同一次请求里,正常响应、错误响应、日志,都应该贴同一个 request_id。类比一下:它就像快递单号——一个包裹从揽收到签收,每个扫描节点都打同一个号,丢件了凭号就能查全链路。
所以前端报错时,只要说一句「request_id=debug-short」,后端就能定位到那一次请求,不用双方对着「大概下午三点那次」瞎猜。
测试这种请求:
{
"resume_text": "太短"
}AnalyzeRequest 里写了:
resume_text: str = Field(min_length=20, max_length=8000)「太短」不满足最小长度,触发 FastAPI 的请求体验证错误,状态码 422。但响应里没有我们自定义的 request_id。
关键是 FastAPI 的执行顺序。正常请求是这样走的:
HTTP 请求
↓
FastAPI 解析 JSON
↓
校验 AnalyzeRequest
↓
校验通过
↓
进入 analyze_resume_api()
↓
调用 service
↓
返回结果参数不合法时,路线在「校验」这里就断了:
HTTP 请求
↓
FastAPI 解析 JSON
↓
校验 AnalyzeRequest
↓
校验失败 → 抛出 RequestValidationError
↓
不会进入 analyze_resume_api()所以 routers.py 里这几行永远不会执行:
request_id = build_request_id(x_request_id)
response.headers["X-Request-ID"] = request_id这就是 422 没带号的真正原因:错误发生在路由函数被调用之前。
用 FastAPI 的:
@app.exception_handler(RequestValidationError)它不是一个普通函数调用,而是注册一条全局规则:以后应用里只要抛出 RequestValidationError,FastAPI 就自动调这个函数处理。
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
...下面这些情况都会触发它:
比如刚才那个 { "resume_text": "太短" }。
request: Request —— 原始 HTTP 请求还没进路由函数,也能从里面拿请求头:
request.headers.get("X-Request-ID")也就是说,前端传来的 X-Request-ID,在这里就能拿到,不用等业务函数。
exc: RequestValidationError —— 校验错误对象通过:
exc.errors()能拿到具体错误明细:
[
{
"loc": ["body", "resume_text"],
"msg": "String should have at least 20 characters",
"type": "string_too_short"
}
]前端拿去提示用户,后端拿去排查,都方便。
原来错误结构比较固定:
def error_detail(request_id, code, message, hint):
...今天改成支持扩展:
def error_detail(
request_id: str,
code: str,
message: str,
hint: str,
**extra: Any,
) -> dict[str, Any]:
return {
"request_id": request_id,
"code": code,
"message": message,
"hint": hint,
**extra,
}这样就能把 errors=exc.errors() 塞进去。最终 422 响应变成:
{
"detail": {
"request_id": "debug-short",
"code": "VALIDATION_ERROR",
"message": "请求参数校验失败",
"hint": "请检查请求字段类型、长度或必填项",
"errors": [...]
}
}422 也带上了 request_id,而且把具体哪儿错说清楚了。
现在项目里的错误分成两类:
MISSING_API_KEY、MODEL_CALL_FAILED。发生在接口函数执行之后,放在 routers.py 里处理。resume_text 太短、字段缺失、类型错。发生在接口函数执行之前,放在 main.py 的全局异常处理器里处理。一句话:业务错误自己管,框架校验错误交给 exception_handler。
到现在,服务能做到:
正常响应 → 有 request_id
模型调用失败 → 有 request_id
API Key 缺失 → 有 request_id
参数校验失败 422 → 也有 request_id
响应头 X-Request-ID → 有
日志按 request_id → 能定位这就是一个完整的错误追踪闭环。
不是所有错误都会进你的业务函数。有些错误生在框架层——比如参数校验失败,它连路由函数都没进去,所以你得用全局异常处理器接住它。
落到 FastAPI 上,就是两层错误机制:
业务错误: 自己在 routers.py 处理
框架校验错误:用 app.exception_handler 处理原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。