本文 AI 產出,尚未審核

FastAPI 教學:Request Form + File 混合處理

簡介

在 Web 應用開發中,表單資料檔案上傳經常需要同時處理。
FastAPI 以 非同步型別提示 為核心,讓我們可以用簡潔的程式碼即時驗證與解析這類混合請求。
本單元將說明如何在 FastAPI 中同時取得 form 欄位與 file,並介紹常見的坑與最佳實踐,幫助你在實務專案中快速上手。

核心概念

1. Form 與 File 的型別宣告

FastAPI 透過 fastapi.Formfastapi.File 以及 starlette.datastructures.UploadFile 來描述請求內容。

  • Form(...) 會把對應的欄位視為 表單字串,支援自動驗證與預設值。
  • File(...) 會把檔案視為 二進位串流,返回 UploadFile 物件,具備 filenamecontent_typeasync read() 方法。

重要:若同時使用 FormFile整個端點的請求內容類型必須是 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 無法解析 FormFile <form enctype="multipart/form-data"> 中加入 method="post",或使用 FormData 物件發送 AJAX。
同步讀取大檔案 使用 file.read() 直接讀入巨檔會佔用大量記憶體。 改用 iter_chunks()shutil.copyfileobj() 逐段寫入磁碟或雲端。
未處理例外 檔案類型或大小不符合需求時,直接回傳 500。 使用 HTTPException 或自訂依賴函式提前驗證,回傳 400/413。
Pydantic Model 直接使用 UploadFile UploadFile 不是 JSON 可序列化的類型,直接回傳會產生錯誤。 僅回傳 filenamecontent_type 等字串資訊,或自行轉換為 bytes
多檔案與單檔混用時的型別不一致 files: List[UploadFile]file: UploadFile 同時出現會造成路由衝突。 為不同情境建立獨立路由,或在同一端點使用 Union[List[UploadFile], UploadFile](需自行判斷)。

最佳實踐

  1. 盡量使用非同步 I/Oawait file.read()await file.seek(),配合 async def 才能發揮 FastAPI 的效能。
  2. 限制檔案大小:透過前端 maxLength 或在後端檢查 len(content),避免 DoS 攻擊。
  3. 使用依賴 (Depends) 抽離驗證邏輯:讓路由函式保持簡潔,驗證與錯誤處理集中管理。
  4. 文件說明寫在程式碼註解或 Pydantic Field:FastAPI 會自動產生 OpenAPI 文件,提升前後端協作效率。
  5. 測試上傳流程:利用 TestClient 實作單元測試,確保 Form 與 File 解析正確。

實際應用場景

場景 為何需要 Form + File 混合 範例實作
商品上架 文字資訊(名稱、價格)與商品圖片同時提交。 範例 3 的 ItemForm + UploadFile
部落格文章發佈 文章內容 (HTML/Markdown) + 首圖或附件。 範例 1 + 自訂驗證圖片類型。
批次匯入資料 CSV 檔案 + 匯入時的參數(如分隔符、編碼)。 使用 Form 傳遞 delimiterencodingFile 上傳 CSV。
用戶個人檔案 使用者提供的個人資訊 + 大頭照。 範例 2 的分段讀取,避免一次載入過大的圖檔。
多語系上傳 同時上傳多個語系的說明文件與相關圖片。 範例 4 的 List[UploadFile] + tags 參數。

總結

FastAPI 讓 Form + File 的混合請求變得既安全又高效。只要掌握以下三個要點,就能在實務專案中順利運用:

  1. 正確宣告 FormFile,確保請求類型為 multipart/form-data
  2. 使用非同步方式 讀取或分段處理檔案,避免記憶體浪費。
  3. 將驗證與商業邏輯分離(如 Depends、自訂例外),提升可讀性與可測試性。

透過本文的範例與最佳實踐,你已具備在 FastAPI 中處理任意混合表單與檔案的能力,接下來就可以把這些技巧套用到自己的 API 中,為使用者提供更完整、友善的上傳體驗。祝開發順利 🚀