FastAPI 教學:Request Form + File 混合處理
簡介
在 Web 應用開發中,表單資料與檔案上傳經常需要同時處理。
FastAPI 以 非同步、型別提示 為核心,讓我們可以用簡潔的程式碼即時驗證與解析這類混合請求。
本單元將說明如何在 FastAPI 中同時取得 form 欄位與 file,並介紹常見的坑與最佳實踐,幫助你在實務專案中快速上手。
核心概念
1. Form 與 File 的型別宣告
FastAPI 透過 fastapi.Form、fastapi.File 以及 starlette.datastructures.UploadFile 來描述請求內容。
Form(...)會把對應的欄位視為 表單字串,支援自動驗證與預設值。File(...)會把檔案視為 二進位串流,返回UploadFile物件,具備filename、content_type與async read()方法。
重要:若同時使用
Form與File,整個端點的請求內容類型必須是multipart/form-data,這是瀏覽器上傳檔案的唯一方式。
2. 非同步讀取檔案
UploadFile 內部使用 SpooledTemporaryFile,在需要時才寫入磁碟,避免一次性載入大檔案。
使用 await file.read() 或 await file.seek(0) 取得檔案內容,務必配合 async 函式,才能發揮效能。
3. 結合 Pydantic Model(可選)
如果表單欄位較多,建議先建立 Pydantic Model,再在端點中以 Depends 注入,讓驗證與文件說明分離。
程式碼範例
下面的範例逐步展示從最簡單到較完整的混合表單處理方式。所有程式碼均可直接放入 main.py,搭配 uvicorn main:app --reload 執行。
範例 1:最基礎的 Form + File
from fastapi import FastAPI, Form, File, UploadFile
app = FastAPI()
@app.post("/upload/simple")
async def upload_simple(
description: str = Form(...), # 必填的文字說明
file: UploadFile = File(...), # 必填的檔案
):
content = await file.read() # 讀取全部二進位資料
size_kb = len(content) / 1024
return {
"filename": file.filename,
"content_type": file.content_type,
"size_kb": round(size_kb, 2),
"description": description,
}
說明:Form(...) 與 File(...) 都使用 ... 表示必填;UploadFile 允許我們在不一次性載入整個檔案的情況下取得大小與名稱。
範例 2:使用非同步分段讀取(適合大檔案)
@app.post("/upload/stream")
async def upload_stream(
title: str = Form(...),
file: UploadFile = File(...),
):
chunk_size = 1024 * 1024 # 1 MB
total_bytes = 0
async for chunk in file.iter_chunks(chunk_size):
total_bytes += len(chunk)
# 這裡可以把 chunk 寫入雲端儲存或資料庫
return {"title": title, "bytes_received": total_bytes}
說明:UploadFile.iter_chunks() 會以指定大小逐段讀取,避免一次佔用過多記憶體。
範例 3:結合 Pydantic Model 與 Depends
from pydantic import BaseModel, Field
from fastapi import Depends
class ItemForm(BaseModel):
name: str = Field(..., description="商品名稱")
price: float = Field(..., gt=0, description="商品價格")
def get_item_form(
name: str = Form(...),
price: float = Form(...),
) -> ItemForm:
return ItemForm(name=name, price=price)
@app.post("/upload/with-model")
async def upload_with_model(
item: ItemForm = Depends(get_item_form),
image: UploadFile = File(...),
):
# 直接使用 Pydantic 驗證過的資料
content = await image.read()
return {
"item": item.dict(),
"filename": image.filename,
"size_kb": round(len(content) / 1024, 2),
}
說明:透過 Depends 把表單欄位映射到 Pydantic Model,讓驗證、文件說明與業務邏輯分離,程式更易維護。
範例 4:多檔案上傳 + 多個 Form 欄位
from typing import List
@app.post("/upload/multiple")
async def upload_multiple(
tags: List[str] = Form([]), # 可接受多個相同名稱的欄位
files: List[UploadFile] = File(...), # 多檔案上傳
):
results = []
for f in files:
data = await f.read()
results.append({
"filename": f.filename,
"size_kb": round(len(data) / 1024, 2),
})
return {"tags": tags, "files": results}
說明:Form([]) 與 File(...) 皆支援列表型別,前端若使用 <input name="tags" multiple> 或多個 <input name="files"> 即可對應。
範例 5:自訂驗證檔案類型
from fastapi import HTTPException, status
def validate_image(file: UploadFile):
if file.content_type not in {"image/jpeg", "image/png"}:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="只接受 JPEG 或 PNG 圖片",
)
@app.post("/upload/image")
async def upload_image(
description: str = Form(...),
image: UploadFile = File(...),
):
validate_image(image)
# 只接受符合類型的檔案
content = await image.read()
return {"description": description, "size_kb": round(len(content) / 1024, 2)}
說明:在端點內自行檢查 content_type,如果不符合需求直接拋出 HTTPException,讓客戶端可以即時得知錯誤。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記設定 multipart/form-data |
前端未正確設定 enctype,導致 FastAPI 無法解析 Form 或 File。 |
在 <form enctype="multipart/form-data"> 中加入 method="post",或使用 FormData 物件發送 AJAX。 |
| 同步讀取大檔案 | 使用 file.read() 直接讀入巨檔會佔用大量記憶體。 |
改用 iter_chunks() 或 shutil.copyfileobj() 逐段寫入磁碟或雲端。 |
| 未處理例外 | 檔案類型或大小不符合需求時,直接回傳 500。 | 使用 HTTPException 或自訂依賴函式提前驗證,回傳 400/413。 |
Pydantic Model 直接使用 UploadFile |
UploadFile 不是 JSON 可序列化的類型,直接回傳會產生錯誤。 |
僅回傳 filename、content_type 等字串資訊,或自行轉換為 bytes。 |
| 多檔案與單檔混用時的型別不一致 | files: List[UploadFile] 與 file: UploadFile 同時出現會造成路由衝突。 |
為不同情境建立獨立路由,或在同一端點使用 Union[List[UploadFile], UploadFile](需自行判斷)。 |
最佳實踐
- 盡量使用非同步 I/O:
await file.read()、await file.seek(),配合async def才能發揮 FastAPI 的效能。 - 限制檔案大小:透過前端
maxLength或在後端檢查len(content),避免 DoS 攻擊。 - 使用依賴 (Depends) 抽離驗證邏輯:讓路由函式保持簡潔,驗證與錯誤處理集中管理。
- 文件說明寫在程式碼註解或 Pydantic Field:FastAPI 會自動產生 OpenAPI 文件,提升前後端協作效率。
- 測試上傳流程:利用
TestClient實作單元測試,確保 Form 與 File 解析正確。
實際應用場景
| 場景 | 為何需要 Form + File 混合 | 範例實作 |
|---|---|---|
| 商品上架 | 文字資訊(名稱、價格)與商品圖片同時提交。 | 範例 3 的 ItemForm + UploadFile。 |
| 部落格文章發佈 | 文章內容 (HTML/Markdown) + 首圖或附件。 | 範例 1 + 自訂驗證圖片類型。 |
| 批次匯入資料 | CSV 檔案 + 匯入時的參數(如分隔符、編碼)。 | 使用 Form 傳遞 delimiter、encoding,File 上傳 CSV。 |
| 用戶個人檔案 | 使用者提供的個人資訊 + 大頭照。 | 範例 2 的分段讀取,避免一次載入過大的圖檔。 |
| 多語系上傳 | 同時上傳多個語系的說明文件與相關圖片。 | 範例 4 的 List[UploadFile] + tags 參數。 |
總結
FastAPI 讓 Form + File 的混合請求變得既安全又高效。只要掌握以下三個要點,就能在實務專案中順利運用:
- 正確宣告
Form與File,確保請求類型為multipart/form-data。 - 使用非同步方式 讀取或分段處理檔案,避免記憶體浪費。
- 將驗證與商業邏輯分離(如
Depends、自訂例外),提升可讀性與可測試性。
透過本文的範例與最佳實踐,你已具備在 FastAPI 中處理任意混合表單與檔案的能力,接下來就可以把這些技巧套用到自己的 API 中,為使用者提供更完整、友善的上傳體驗。祝開發順利 🚀