FastAPI 服务用 contextvars 存 request_id,在路由函数里能取到,扔进 BackgroundTasks 和 asyncio.create_task 之后拿到的是空值,日志里这条链路全是短横线,中间件里明明已经 set 过了。
一开始怀疑是「BackgroundTasks 在响应发出后才运行,此时请求上下文已经结束」,查了一圈,方向不对。
真正麻烦的地方在于,几个原因的现象几乎一样。contextvars 在新建任务时会拷贝当前上下文,但后台任务由线程池执行时不继承。少数情况下是中间件用 BaseHTTPMiddleware 时上下文在响应返回后就被清理。
少数情况下是日志格式化器在模块导入时就绑定了空值。
先在路由内打印当前上下文值
python -c "import contextvars;rid=contextvars.ContextVar('rid');rid.set('req-8821');print(rid.get())"
接着在新任务里显式拷贝上下文
python -c "import asyncio,contextvars;rid=contextvars.ContextVar('rid');rid.set('req-8821');ctx=contextvars.copy_context();print(ctx.run(rid.get))"
后台任务改成传参而不是靠上下文
python -c "print({'task_args': ['req-8821']})"
中间件换成纯 ASGI 实现避免上下文提前清理
python -c "print('use raw ASGI middleware instead of BaseHTTPMiddleware')"
别只看报错消失,验证要多做一步。路由、后台任务、异步子任务三条路径打出的 request_id 一致,日志能按同一 id 串起来,不再出现空值。
上下文只在同一个执行体内自动传递,跨线程池或跨任务边界要显式拷贝或传参,靠隐式继承迟早会断。报错文案容易误导,真正的原因往往在相邻那一层的日志里。
