本文 AI 產出,尚未審核

FastAPI – Multipart/form‑data 支援完整指南


簡介

在 Web 應用中,檔案上傳是最常見的需求之一。
傳統的 HTML 表單會以 multipart/form-data 編碼方式將文字欄位與二進位檔案一起送出,伺服器端必須能正確解析這種複雜的請求。

FastAPI 以 Starlette 為底層,天然支援 multipart/form-data,讓開發者只要寫少量的型別宣告與程式碼,就能安全、快速地處理檔案與表單資料。
本篇文章將從 概念實作範例常見陷阱 以及 最佳實務 逐步說明,幫助初學者到中階開發者在實務專案中自信使用 FastAPI 處理檔案上傳。


核心概念

1. 為什麼需要 multipart/form-data

  • 混合資料:同一個請求可能同時包含文字欄位(如 usernamedescription)與檔案(如圖片、PDF)。
  • 二進位安全:純文字的 application/x-www-form-urlencoded 會把二進位資料編碼成字串,會造成檔案損毀;multipart/form-data 會以 boundary 分隔每個部件,保留原始位元。

2. FastAPI 如何抽象化

FastAPI 透過 Pydantic 來驗證文字欄位,透過 StarletteUploadFile 物件來處理檔案。
只要在路由函式的參數上加上型別註記,框架會自動:

  1. 解析 multipart 請求
  2. 把文字欄位轉成對應的 Python 基本型別或 Pydantic 模型
  3. 把檔案包裝成 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(需自行封裝)。

最佳實踐

  1. 使用 async 方式讀寫:保持與 FastAPI 其餘非阻塞程式碼一致。
  2. 限制檔案大小:在路由層面或使用 middleware 檢查 request.headers["content-length"]
  3. 驗證 MIME typefile.content_type 應與允許的副檔名對照,避免偽裝檔案。
  4. 儲存至外部服務:直接把 UploadFile.file 串流至 S3、Google Cloud Storage,減少本機磁碟 I/O。
  5. 寫測試:使用 TestClientfiles= 參數模擬 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 完美抽象化。
  • 只要在路由參數上使用 FormFile(或 List[UploadFile]),框架會自動完成 解析、驗證、非阻塞讀寫
  • 實務開發中,需注意 檔案大小、MIME 類型、資源釋放,並盡可能 分段讀取 或直接 串流至雲端
  • 透過上述範例與最佳實踐,你可以快速建置 安全、效能佳 的檔案上傳 API,並在各種業務場景下靈活擴展。

祝你在 FastAPI 的檔案上傳開發旅程中,玩得開心、寫得順手! 🚀