首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >让每一次错误都能被 request_id 追踪

让每一次错误都能被 request_id 追踪

原创
作者头像
dsy
发布2026-08-15 22:20:33
发布2026-08-15 22:20:33
860
举报

上一章收尾时埋了个钩子:给每次请求加 request_id。落地之后,正常响应、模型调用失败、API Key 缺失,都已经带上了同一个号。前端一报错,把号甩给后端,就能去日志里捞出同一次请求的全过程。

但前两天联调,我故意传了段超短简历,想看看参数校验报错长什么样。返回的 422 里,request_id 没了。

这就有意思了。同一个服务,为什么校验错误偏偏不带号?这一篇就把这个漏洞补上。


一、先说清楚 request_id 是什么

它不是用户 ID,也不是业务 ID,是用来标识「这一次 HTTP 请求」的。

同一次请求里,正常响应、错误响应、日志,都应该贴同一个 request_id。类比一下:它就像快递单号——一个包裹从揽收到签收,每个扫描节点都打同一个号,丢件了凭号就能查全链路。

所以前端报错时,只要说一句「request_id=debug-short」,后端就能定位到那一次请求,不用双方对着「大概下午三点那次」瞎猜。


二、问题出在哪:422 没带号

测试这种请求:

代码语言:json
复制
{
  "resume_text": "太短"
}

AnalyzeRequest 里写了:

代码语言:python
复制
resume_text: str = Field(min_length=20, max_length=8000)

「太短」不满足最小长度,触发 FastAPI 的请求体验证错误,状态码 422。但响应里没有我们自定义的 request_id


三、为什么 422 进不了 routers.py

关键是 FastAPI 的执行顺序。正常请求是这样走的:

代码语言:shell
复制
HTTP 请求
  ↓
FastAPI 解析 JSON
  ↓
校验 AnalyzeRequest
  ↓
校验通过
  ↓
进入 analyze_resume_api()
  ↓
调用 service
  ↓
返回结果

参数不合法时,路线在「校验」这里就断了:

代码语言:shell
复制
HTTP 请求
  ↓
FastAPI 解析 JSON
  ↓
校验 AnalyzeRequest
  ↓
校验失败 → 抛出 RequestValidationError
  ↓
不会进入 analyze_resume_api()

所以 routers.py 里这几行永远不会执行:

代码语言:python
复制
request_id = build_request_id(x_request_id)
response.headers["X-Request-ID"] = request_id

这就是 422 没带号的真正原因:错误发生在路由函数被调用之前。


四、解法:全局异常处理器

用 FastAPI 的:

代码语言:python
复制
@app.exception_handler(RequestValidationError)

它不是一个普通函数调用,而是注册一条全局规则:以后应用里只要抛出 RequestValidationError,FastAPI 就自动调这个函数处理。


五、它什么时候触发

代码语言:python
复制
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
    request: Request,
    exc: RequestValidationError,
) -> JSONResponse:
    ...

下面这些情况都会触发它:

  • 字段缺失
  • 字段类型错
  • 字符串太短 / 太长
  • JSON 格式不合法
  • 请求体不符合 Pydantic 模型

比如刚才那个 { "resume_text": "太短" }


六、两个参数是关键

1. request: Request —— 原始 HTTP 请求

还没进路由函数,也能从里面拿请求头:

代码语言:python
复制
request.headers.get("X-Request-ID")

也就是说,前端传来的 X-Request-ID,在这里就能拿到,不用等业务函数。

2. exc: RequestValidationError —— 校验错误对象

通过:

代码语言:python
复制
exc.errors()

能拿到具体错误明细:

代码语言:json
复制
[
  {
    "loc": ["body", "resume_text"],
    "msg": "String should have at least 20 characters",
    "type": "string_too_short"
  }
]

前端拿去提示用户,后端拿去排查,都方便。


七、让 error_detail 能带额外字段

原来错误结构比较固定:

代码语言:python
复制
def error_detail(request_id, code, message, hint):
    ...

今天改成支持扩展:

代码语言:python
复制
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 响应变成:

代码语言:json
复制
{
  "detail": {
    "request_id": "debug-short",
    "code": "VALIDATION_ERROR",
    "message": "请求参数校验失败",
    "hint": "请检查请求字段类型、长度或必填项",
    "errors": [...]
  }
}

422 也带上了 request_id,而且把具体哪儿错说清楚了。


八、错误处理的两类分工

现在项目里的错误分成两类:

  1. 路由函数内部错误:比如 MISSING_API_KEYMODEL_CALL_FAILED。发生在接口函数执行之后,放在 routers.py 里处理。
  2. 请求校验错误:比如 resume_text 太短、字段缺失、类型错。发生在接口函数执行之前,放在 main.py 的全局异常处理器里处理。

一句话:业务错误自己管,框架校验错误交给 exception_handler


九、追踪闭环补完了

到现在,服务能做到:

代码语言:shell
复制
正常响应            → 有 request_id
模型调用失败        → 有 request_id
API Key 缺失       → 有 request_id
参数校验失败 422    → 也有 request_id
响应头 X-Request-ID → 有
日志按 request_id   → 能定位

这就是一个完整的错误追踪闭环。


十、这一篇想说清楚一件事

不是所有错误都会进你的业务函数。有些错误生在框架层——比如参数校验失败,它连路由函数都没进去,所以你得用全局异常处理器接住它。

落到 FastAPI 上,就是两层错误机制:

代码语言:shell
复制
业务错误:    自己在 routers.py 处理
框架校验错误:用 app.exception_handler 处理

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先说清楚 request_id 是什么
  • 二、问题出在哪:422 没带号
  • 三、为什么 422 进不了 routers.py
  • 四、解法:全局异常处理器
  • 五、它什么时候触发
  • 六、两个参数是关键
    • 1. request: Request —— 原始 HTTP 请求
    • 2. exc: RequestValidationError —— 校验错误对象
  • 七、让 error_detail 能带额外字段
  • 八、错误处理的两类分工
  • 九、追踪闭环补完了
  • 十、这一篇想说清楚一件事
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档