FastAPI 接收 list[UploadFile] 时为何返回 422?
结论
{"loc":["body","files"],"msg":"field required"} 表示 FastAPI 没有在请求体中找到名为 files 的上传字段。接收端需要将 files 声明为文件表单字段,发送端则要通过 httpx 的 files 参数,以 multipart/form-data 发送同名字段。上传多个文件时,使用多个 ("files", ...) 项。
Swagger 和 Postman 上传成功,说明接收端至少能处理它们发出的请求。当前错误指向请求体缺少 files,因此应检查 httpx 实际发送的请求,以及文件流的状态。
接收端声明
用 File(...) 将文件列表声明为文件字段:
from fastapi import Depends, File, UploadFile
@router.post("/{connector_id}/upload")
@router.post("/{connector_id}/upload/{upload_id}")
async def upload(
tenant_id: str,
connector_id: str,
files: list[UploadFile] = File(...),
upload_id: str | None = None,
controller: ConnectorController = Depends(get_controller),
) -> dict:
...
File(...) 会让 FastAPI 按文件表单解析 files。展示的路由路径中没有 tenant_id,所以 FastAPI 会将它作为必需的查询参数处理。若缺少该参数,校验错误的 loc 通常会显示相应字段;当前错误指出的是 body.files 缺失。
发送多个文件
把每个文件作为一个独立的同名 multipart 字段传给 httpx:
multipart_files = []
for upload in files:
await upload.seek(0)
multipart_files.append(
(
"files",
(
upload.filename,
upload.file,
upload.content_type or "application/octet-stream",
),
)
)
response = await self._client.post(url=url, files=multipart_files)
每项的外层名称都要是 files,这样才能对应接收端的参数名。files 参数会生成 multipart 请求体和相应的 Content-Type。不要手动把请求头设为 application/json,也不要写死 multipart 的 Content-Type,否则请求可能缺少正确的 boundary。
原问题中的 UploadFile 来自 BytesIO。发送前调用 await upload.seek(0),可以让读取从文件开头开始。如果流已经读到末尾,接收端可能收到空内容。还要确认 upload.file 是可读取的文件对象,并检查实际发出的请求是否仍包含这些文件项。
仍然报错时检查
- 发送前检查
multipart_files中每个字段名是否都是files,而不是file、files[]或其他名称。 - 检查最终请求是否使用
files=multipart_files,并确认没有其他代码将请求体替换为 JSON。 - 确认请求 URL 指向预期端点,并包含必需的
tenant_id查询参数。若缺少该参数,错误详情通常会显示对应的查询字段。 - 查看服务端收到的
Content-Type是否为带 boundary 的 multipart 类型,并检查客户端或中间层是否改写了请求头或请求体。
如果请求格式看起来正确,但仍然出现相同的 body.files 错误,就需要检查 httpx 实际发出的请求内容。在客户端调试器中看到 Python 对象是元组,并不能确认请求体里确实包含名为 files 的 multipart 字段。
备注:内容仅供参考。
