如何在FastAPI中使用await request.form()同时保留Form参数校验功能?
FastAPI中间件中复用表单数据的解决方案
问题场景
在FastAPI中编写请求日志中间件时,调用await request.form()会消费掉请求体的所有数据,导致后续接口通过Form(...)获取参数时返回422 Unprocessable Entity错误。测试代码及现象如下:
测试代码
import uvicorn from fastapi import FastAPI, Form app = FastAPI() @app.middleware('http') async def log_request(request, call_next): print(await request.form()) # 此处消费了请求体 return await call_next(request) @app.post('/') async def test(id: int = Form(...)): return {'id': id} if __name__ == '__main__': uvicorn.run(app, host='0.0.0.0', port=80)
请求命令
curl --data id=1 http://localhost
错误现象
执行请求后返回422 Unprocessable Entity,注释掉print(await request.form())则正常返回200 OK。
解决方法
核心思路是先保存请求体原始数据,让后续流程可以重复读取,具体有两种实现方式:
方法一:手动复用请求体原始数据
先读取请求体的字节数据,解析为表单后,将原始字节数据重新存入请求对象的内部属性,供后续流程读取:
import uvicorn from fastapi import FastAPI, Form from starlette.requests import Request app = FastAPI() @app.middleware('http') async def log_request(request: Request, call_next): # 读取原始请求体字节数据 raw_body = await request.body() # 解析为表单数据用于日志 form_data = await request.form() print(form_data) # 将原始字节数据回写,让后续流程能再次读取 request._body = raw_body return await call_next(request) @app.post('/') async def test(id: int = Form(...)): return {'id': id} if __name__ == '__main__': uvicorn.run(app, host='0.0.0.0', port=80)
方法二:使用依赖项替代中间件(更推荐)
如果仅需记录请求参数,用FastAPI的依赖项可以避免直接操作请求内部属性,更符合框架设计规范:
import uvicorn from fastapi import FastAPI, Form, Depends from starlette.requests import Request app = FastAPI() async def log_form_data(request: Request): # 依赖项中读取表单数据,框架会自动处理复用逻辑 form_data = await request.form() print(form_data) return form_data @app.post('/') async def test(id: int = Form(...), _=Depends(log_form_data)): return {'id': id} if __name__ == '__main__': uvicorn.run(app, host='0.0.0.0', port=80)
关键说明
request.body()读取的是原始字节数据,不会一次性消费请求流;而request.form()会消费请求体流,因此必须先保存原始数据才能复用。- 方法一中操作
request._body是直接修改Starlette的内部属性,虽然可行,但方法二的依赖项方式更优雅,也更适配FastAPI的生命周期逻辑。
内容的提问来源于stack exchange,提问作者OrthoPole
相关产品推荐
相关产品推荐

