FastAPI – Multipart/form‑data 支援完整指南
簡介
在 Web 應用中,檔案上傳是最常見的需求之一。
傳統的 HTML 表單會以 multipart/form-data 編碼方式將文字欄位與二進位檔案一起送出,伺服器端必須能正確解析這種複雜的請求。
FastAPI 以 Starlette 為底層,天然支援 multipart/form-data,讓開發者只要寫少量的型別宣告與程式碼,就能安全、快速地處理檔案與表單資料。
本篇文章將從 概念、實作範例、常見陷阱 以及 最佳實務 逐步說明,幫助初學者到中階開發者在實務專案中自信使用 FastAPI 處理檔案上傳。
核心概念
1. 為什麼需要 multipart/form-data
- 混合資料:同一個請求可能同時包含文字欄位(如
username、description)與檔案(如圖片、PDF)。 - 二進位安全:純文字的
application/x-www-form-urlencoded會把二進位資料編碼成字串,會造成檔案損毀;multipart/form-data會以 boundary 分隔每個部件,保留原始位元。
2. FastAPI 如何抽象化
FastAPI 透過 Pydantic 來驗證文字欄位,透過 Starlette 的 UploadFile 物件來處理檔案。
只要在路由函式的參數上加上型別註記,框架會自動:
- 解析 multipart 請求
- 把文字欄位轉成對應的 Python 基本型別或 Pydantic 模型
- 把檔案包裝成
UploadFile(支援 async 讀寫)
重點:
UploadFile不是完整的檔案內容,而是指向暫存檔的檔案描述子,只有在需要時才讀取,對記憶體友好。
3. 基本使用方式
| 參數類型 | 宣告方式 | 取得方式 |
|---|---|---|
| 單一文字欄位 | name: str = Form(...) |
name 直接使用 |
| 多筆文字欄位 | tags: List[str] = Form([]) |
tags 為 list |
| 單一檔案 | file: UploadFile = File(...) |
await file.read() |
| 多筆檔案 | files: List[UploadFile] = File([]) |
逐一 await f.read() |
程式碼範例
以下示範 5 個常見情境,請自行在 main.py 中測試,並使用 uvicorn main:app --reload 啟動。
1️⃣ 單一檔案 + 文字欄位
from fastapi import FastAPI, File, Form, UploadFile
from fastapi.responses import JSONResponse
app = FastAPI()
@app.post("/upload-single")
async def upload_single(
username: str = Form(...), # 必填文字欄位
description: str = Form(None), # 可選文字欄位
file: UploadFile = File(...), # 必填檔案
):
# 讀取檔案內容(示範只取前 100 bytes)
content = await file.read()
size = len(content)
# 立即釋放資源
await file.close()
return JSONResponse(
{"username": username,
"description": description,
"filename": file.filename,
"content_type": file.content_type,
"size_bytes": size}
)
說明:
Form(...)與File(...)都是 必填,若想讓欄位可選,給予預設值或None即可。
2️⃣ 多筆檔案上傳
from typing import List
@app.post("/upload-multiple")
async def upload_multiple(
files: List[UploadFile] = File(...), # 接收多個檔案
):
results = []
for f in files:
data = await f.read()
results.append({
"filename": f.filename,
"content_type": f.content_type,
"size": len(data)
})
await f.close()
return {"files": results}
技巧:前端表單的
<input type="file" multiple>會自動對應到List[UploadFile]。
3️⃣ 結合 Pydantic 模型與檔案
from pydantic import BaseModel, Field
class ItemInfo(BaseModel):
title: str = Field(..., example="My Photo")
tags: List[str] = Field(default_factory=list)
@app.post("/upload-with-model")
async def upload_with_model(
info: ItemInfo = Form(...), # 把 JSON 形式的表單欄位映射成模型
image: UploadFile = File(...),
):
# 直接使用 Pydantic 產生的資料
return {
"title": info.title,
"tags": info.tags,
"filename": image.filename,
"size": (await image.read()).__len__()
}
重點:
Form(...)只能接收 字串,若想傳遞 JSON 必須在前端把物件JSON.stringify後放入隱藏欄位或使用application/json+multipart的混合方式(稍後說明)。
4️⃣ 大檔案的分段讀取(避免一次載入全部)
@app.post("/upload-stream")
async def upload_stream(file: UploadFile = File(...)):
chunk_size = 1024 * 1024 # 1 MB
total = 0
async for chunk in file.iter_chunks(chunk_size):
total += len(chunk)
# 這裡可以把 chunk 寫入雲端儲存或磁碟
await file.close()
return {"filename": file.filename, "total_bytes": total}
說明:使用
iter_chunks可 逐塊 讀取檔案,對於上傳大型影片或備份檔非常有幫助。
5️⃣ 同時處理文字欄位、單檔與多檔
@app.post("/complex")
async def complex_upload(
user_id: int = Form(...),
comment: str = Form(None),
avatar: UploadFile = File(...), # 單一頭像
attachments: List[UploadFile] = File([]) # 任意多筆附件
):
avatar_info = {"filename": avatar.filename, "size": (await avatar.read()).__len__()}
await avatar.close()
attach_info = []
for a in attachments:
data = await a.read()
attach_info.append({"filename": a.filename, "size": len(data)})
await a.close()
return {
"user_id": user_id,
"comment": comment,
"avatar": avatar_info,
"attachments": attach_info,
}
實務:此結構常見於 社群平台、客服系統,一次提交文字說明、頭像與多張圖片。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的解決方式 |
|---|---|---|
忘記加 await file.read() |
直接使用 file.file.read() 會返回同步檔案物件,失去 async 優勢,且在高併發下會阻塞事件迴圈。 |
使用 await file.read() 或 async for chunk in file.iter_chunks()。 |
| 一次讀取過大檔案 | await file.read() 會把整個檔案載入記憶體,若檔案超過數十 MB 甚至 GB,會導致 OOM。 |
分塊讀取(iter_chunks)或直接將 UploadFile.file 交給外部儲存服務(S3、Azure Blob)。 |
| 未限制檔案類型 | 攻擊者可能上傳惡意執行檔或過大檔案,造成安全或資源問題。 | 在路由前加入 檔案類型檢查(file.content_type)與 大小限制(await file.seek(0, 2) 取得大小)。 |
| 表單欄位名稱衝突 | 同名的文字欄位與檔案欄位會被覆寫,導致資料遺失。 | 在前端明確命名,例如 profile_picture vs profile_picture_desc。 |
| 忘記關閉檔案 | UploadFile 會在 GC 時關閉,但在長時間服務中會佔用檔案描述子。 |
顯式呼叫 await file.close(),或使用 with 風格的 async with(需自行封裝)。 |
最佳實踐
- 使用 async 方式讀寫:保持與 FastAPI 其餘非阻塞程式碼一致。
- 限制檔案大小:在路由層面或使用 middleware 檢查
request.headers["content-length"]。 - 驗證 MIME type:
file.content_type應與允許的副檔名對照,避免偽裝檔案。 - 儲存至外部服務:直接把
UploadFile.file串流至 S3、Google Cloud Storage,減少本機磁碟 I/O。 - 寫測試:使用
TestClient的files=參數模擬 multipart 請求,確保 API 行為如預期。
from fastapi.testclient import TestClient
client = TestClient(app)
def test_upload_single():
response = client.post(
"/upload-single",
data={"username": "alice"},
files={"file": ("test.txt", b"hello world", "text/plain")}
)
assert response.status_code == 200
assert response.json()["filename"] == "test.txt"
實際應用場景
| 場景 | 為何適合使用 FastAPI multipart | 可能的進階需求 |
|---|---|---|
| 社群平台的貼文 | 同時上傳文字、圖片、影片。 | 圖片壓縮、影片轉碼、即時 CDN 上傳。 |
| 企業內部文件管理 | 上傳 PDF、Excel,並保存表單資訊(部門、標題)。 | 權限驗證、病毒掃描、版本控制。 |
| 醫療影像系統 | 大型 DICOM 檔案與患者資訊同時送出。 | 分段寫入分散式儲存、加密傳輸。 |
| AI 模型訓練平台 | 使用者上傳訓練資料集(CSV+圖片)。 | 立即觸發背景工作(Celery、RQ)進行前處理。 |
| 電商商品上架 | 商品描述 + 多張商品圖 + PDF 規格書。 | 圖片自動產生縮圖、文件轉 PDF、即時預覽。 |
在上述案例中,FastAPI 的 自動文件驗證 + 非阻塞 I/O 能讓開發者專注於業務邏輯,而不是繁雜的 multipart 解析程式碼。
總結
multipart/form-data是處理 文字 + 二進位檔案 的標準編碼方式,FastAPI 以 Starlette 的 UploadFile 完美抽象化。- 只要在路由參數上使用
Form、File(或List[UploadFile]),框架會自動完成 解析、驗證、非阻塞讀寫。 - 實務開發中,需注意 檔案大小、MIME 類型、資源釋放,並盡可能 分段讀取 或直接 串流至雲端。
- 透過上述範例與最佳實踐,你可以快速建置 安全、效能佳 的檔案上傳 API,並在各種業務場景下靈活擴展。
祝你在 FastAPI 的檔案上傳開發旅程中,玩得開心、寫得順手! 🚀