FastAPI 接收客户端上传文件实战:File、UploadFile 与 multipart/form-data 机制
本文基于 FastAPI 官方教程「Request 中的文件」,系统讲解如何在 FastAPI 接口中声明并处理客户端上传的文件:从安装 python-multipart 前置依赖,到用 bytes 与 UploadFile 两种方式接收文件、声明可选上传与多文件批量上传,并结合 fastapi/params.py 与 fastapi/datastructures.py 的源码,剖析 File 参数继承体系与 UploadFile 的底层实现。读完本文,你将掌握 FastAPI 文件上传接口的完整写法,并理解其背后的 HTTP 表单编码机制与线程池执行细节。
一、前置条件:安装 python-multipart
要接收客户端上传的文件,必须先安装 python-multipart 包。将其加入项目:
$ uv add python-multipart
之所以有这个依赖,是因为上传的文件会以**表单数据(form data)**的方式随请求发送,而 FastAPI 需要 python-multipart 来解析 multipart/form-data 编码的请求体。
二、导入 File 并定义文件参数
从 fastapi 导入 File 和 UploadFile,然后像使用 Body 或 Form 一样声明文件参数。以下代码来自仓库中的可运行示例 tutorial001_an_py310.py:
from typing import Annotated
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
return {"filename": file.filename}
两点关键说明:
File的继承关系:File是一个直接继承自Form的类。这一点可以在源码中得到印证:fastapi/params.py 中定义了class Form(Body),随后在 fastapi/params.py 定义了class File(Form),并且File将默认media_type固定为"multipart/form-data"(见 fastapi/params.py)。- 导入的是"函数"而非"类":当从
fastapi导入Query、Path、File等名称时,它们实际上是返回对应参数类(最终都派生自 Pydantic 的FieldInfo)的函数,Annotated[bytes, File()]这种写法正是利用函数调用结果携带参数元信息。
提示:声明文件参数时必须使用
File。因为如果不用File,该参数会被 FastAPI 解释为 Query 参数或 Body(JSON)参数,而不是表单中的文件字段。
当参数类型声明为 bytes 时,FastAPI 会自动读取上传的文件,并把文件内容以 bytes 的形式传给你。需要注意的是,这意味着整个文件内容都会驻留在内存中——对较小的文件这完全没问题,但很多场景下使用 UploadFile 会更合适。
三、使用 UploadFile 声明文件参数
在上面的示例中,/uploadfile/ 路由直接以 UploadFile 作为参数类型。相比 bytes,使用 UploadFile 有以下优点:
- 参数不需要使用
File()作为默认值; - 底层使用"spooled(池化/暂存)"文件:文件先保存在内存中,当超过一定大小上限后自动转存到磁盘;
- 因此对图片、视频、大型二进制文件等大文件也能良好工作,不会耗尽内存;
- 可以读取上传文件的元数据;
- 提供文件类似(file-like)的
async接口; - 它暴露了一个真实的 Python
SpooledTemporaryFile对象,可以直接传给其他期望接收"文件类对象"的库。
3.1 UploadFile 的属性
UploadFile 提供以下属性:
filename:str,上传文件的原始名称(例如myimage.jpg);content_type:str,内容类型(MIME 类型 / 媒体类型,例如image/jpeg);file:SpooledTemporaryFile(文件类对象)。这是真正的 Python 文件对象,可以直接传给期望接收文件类对象的其他函数或库。
从源码 fastapi/datastructures.py 还可以看到,FastAPI 的 UploadFile 在 Starlette 基础上还标注了 size(文件字节大小)和 headers(请求头)等属性,且每个属性都带有 Doc 注解,会被文档工具用于生成说明。
3.2 UploadFile 的 async 方法
UploadFile 提供以下 async 方法,它们全部是对底层文件对象(内部为 SpooledTemporaryFile)对应方法的封装(见 fastapi/datastructures.py):
write(data):将data(bytes)写入文件;read(size):从文件读取size(int)个字节,默认size=-1表示读取到结尾;seek(offset):定位到文件的字节位置offset(int)。例如await myfile.seek(0)会回到文件开头——当你执行过一次await myfile.read()之后需要再次读取内容时,这一步尤其有用;close():关闭文件。
由于这些都是 async 方法,调用时必须使用 await。例如在 async 路径操作函数中读取内容:
contents = await myfile.read()
而如果你在一个普通的 def(同步)路径操作函数中,可以直接访问 UploadFile.file,例如:
contents = myfile.file.read()
技术细节(async):使用这些
async方法时,FastAPI 会在线程池(threadpool)中执行底层的文件操作并等待其完成,从而避免阻塞事件循环。
技术细节(Starlette):FastAPI 的
UploadFile直接继承自 Starlette 的UploadFile,并补充了若干部件使其兼容 Pydantic 和 FastAPI 的其他部分。源码 fastapi/datastructures.py 中的__get_pydantic_json_schema__方法返回{"type": "string", "contentMediaType": "application/octet-stream"},这使得 OpenAPI 文档中上传文件字段被描述为二进制字符串;_validate类方法则负责在 Pydantic 校验时确保传入的确实是UploadFile实例。
四、什么是"表单数据"
HTML 表单(<form></form>)向服务器发送数据的方式通常采用一种"特殊的"编码,与 JSON 不同。FastAPI 会确保从正确的位置读取这些数据,而不是去解析 JSON。
技术细节:表单数据如果不包含文件,通常以媒体类型
application/x-www-form-urlencoded编码;但如果表单包含文件,则会以multipart/form-data编码。当你使用File时,FastAPI 便知道必须从请求体的正确部分(而非 JSON)获取文件。这一点与源码中三类参数的默认media_type一一对应:Body默认为application/json(fastapi/params.py)、Form默认为application/x-www-form-urlencoded(fastapi/params.py)、File默认为multipart/form-data(fastapi/params.py)。
警告:你可以在一个路径操作中同时声明多个
File和Form参数,但不能同时声明期望 JSON 的Body字段——因为请求体将以multipart/form-data而非application/json编码。这不是 FastAPI 的限制,而是 HTTP 协议本身的约束。
五、可选的文件上传
通过标准类型标注并设置默认值 None,可以让文件变为可选。示例见 tutorial001_02_an_py310.py:
from typing import Annotated
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: Annotated[bytes | None, File()] = None):
if not file:
return {"message": "No file sent"}
else:
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile | None = None):
if not file:
return {"message": "No upload file sent"}
else:
return {"filename": file.filename}
对 bytes 类型使用 Annotated[bytes | None, File()] = None,对 UploadFile 类型直接写 UploadFile | None = None 即可,客户端未上传文件时参数取值为 None。
六、为 UploadFile 附加额外元数据
你同样可以在 UploadFile 上使用 File(),例如为上传字段附加描述等元数据。示例见 tutorial001_03_an_py310.py:
from typing import Annotated
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]):
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(
file: Annotated[UploadFile, File(description="A file read as UploadFile")],
):
return {"filename": file.filename}
File() 接受 description、alias、examples 等参数,这些元数据会体现在自动生成的 OpenAPI 文档中,便于前端在 Swagger UI 中理解该文件字段。
七、多文件上传
可以同时上传多个文件,它们会被映射到同一个以表单数据发送的"表单字段"。做法是声明一个 bytes 或 UploadFile 的列表。示例见 tutorial002_an_py310.py,其中还包含一个可直接用于浏览器测试的 HTML 表单页面:
from typing import Annotated
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.post("/files/")
async def create_files(files: Annotated[list[bytes], File()]):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: list[UploadFile]):
return {"filenames": [file.filename for file in files]}
@app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
你将按照声明的类型,得到一个 bytes 或 UploadFile 的 list。
技术细节:也可以从
starlette.responses导入HTMLResponse。FastAPI 为了方便开发者,同样通过fastapi.responses提供这些starlette.responses中的响应类;但其中大多数响应类直接来自 Starlette。
7.1 多文件上传并附加额外元数据
与前面一样,你也可以在 UploadFile 上使用 File() 设置额外参数。示例见 tutorial003_an_py310.py:
@app.post("/files/")
async def create_files(
files: Annotated[list[bytes], File(description="Multiple files as bytes")],
):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(
files: Annotated[
list[UploadFile], File(description="Multiple files as UploadFile")
],
):
return {"filenames": [file.filename for file in files]}
八、小结
使用 File、bytes 和 UploadFile 这三个要素,即可在 FastAPI 中声明以表单数据(multipart/form-data)方式发送的可上传文件:
- 小文件、只需字节流:声明为
Annotated[bytes, File()],FastAPI 自动读入内存; - 大文件、需要元数据:使用
UploadFile,获得内存/磁盘自动暂存、文件名与内容类型元数据、可复用的文件对象; - 可选上传:类型加
| None并给默认值None; - 多文件上传:声明
list[bytes]或list[UploadFile]; - 附加约束与说明:通过
File(description=..., alias=...)等参数补充元数据,自动进入 OpenAPI 文档。
更多示例可参考 docs_src/request_files/ 目录下的完整可运行代码,以及对应的英文原版教程页面;核心实现见 fastapi/params.py(Form/File 参数类)与 fastapi/datastructures.py(UploadFile 类)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00