PAYATHON 2026

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 字段。

备注:内容仅供参考。