本文 AI 產出,尚未審核

FastAPI – 表單與檔案上傳(Form & File Upload)

主題:限制檔案大小


簡介

在 Web 應用中,表單與檔案上傳是最常見的互動方式之一。若未對上傳檔案的大小進行適當限制,可能會導致 伺服器資源耗盡、磁碟空間被填滿,甚至成為 DoS(Denial‑of‑Service) 攻擊的入口。FastAPI 內建了與 Starlette 整合的請求處理機制,讓我們可以輕鬆地在路由層或全域層面加入檔案大小驗證。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你完成 安全、彈性的檔案大小限制,讓你的 API 在上傳功能上更可靠。


核心概念

1. 為什麼要在 FastAPI 中限制檔案大小?

風險 可能的影響
資源耗盡 大檔案會佔用大量記憶體與磁碟,導致其他請求變慢或失敗
安全漏洞 攻擊者可利用無限制上傳執行 DoS 或儲存惡意檔案
使用者體驗 使用者若上傳過大的檔案,往往會等很久才收到錯誤回應,影響滿意度

因此,我們需要在 接收檔案前 就先檢查大小,或在 流式讀取時 立即中斷過大的傳輸。

2. FastAPI 處理檔案的兩種方式

  1. 一次性載入(UploadFile

    • FastAPI 會先把檔案暫存於磁碟(或記憶體),然後把 UploadFile 物件傳給 endpoint。
    • 適合小檔案或需要直接存取檔案內容的情境。
  2. 流式讀取(StreamingFormDataParser

    • 透過 request.stream() 逐塊讀取資料,能在讀取過程中即時檢查大小。
    • 適合大檔案或需要「邊讀邊驗」的需求。

以下範例會同時展示這兩種方式的 檔案大小限制

3. 兩種實作策略

策略 說明 適用情境
全域 Middleware 在請求進入路由前攔截,檢查 Content‑Length 或實際讀取的位元組數 所有上傳皆需統一上限
路由層驗證 在特定 endpoint 內自行檢查檔案大小 部分上傳有不同上限或需要客製化錯誤訊息

程式碼範例

以下程式碼均以 Python 3.11FastAPI 0.110 為基礎,請先安裝相依套件:

pip install fastapi[all] python-multipart

範例 1️⃣:使用 UploadFile 搭配 Content‑Length Header 的全域 Middleware

# main.py
from fastapi import FastAPI, Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware

app = FastAPI()

MAX_SIZE = 5 * 1024 * 1024  # 5 MB

class MaxUploadSizeMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # 1. 先檢查 Header 是否提供 Content‑Length
        content_length = request.headers.get("content-length")
        if content_length and int(content_length) > MAX_SIZE:
            raise HTTPException(
                status_code=413,
                detail=f"檔案過大,最大允許 {MAX_SIZE // (1024*1024)} MB"
            )
        # 2. 若 Header 沒有或不可信,仍需要在讀取時限制
        response = await call_next(request)
        return response

app.add_middleware(MaxUploadSizeMiddleware)

@app.post("/upload")
async def upload_file(file: bytes = File(...)):
    # 若走到這裡,表示檔案大小已通過檢查
    return {"filename": "unknown", "size": len(file)}

說明

  • Content‑Length 是最直接的檢查方式,但若客戶端未傳或偽造,仍需在後端讀取時再次驗證。
  • HTTP 413 Payload Too Large 為標準的「檔案過大」回應碼。

範例 2️⃣:路由層檢查 UploadFile 的實際大小

# main.py
from fastapi import FastAPI, UploadFile, File, HTTPException

app = FastAPI()
MAX_SIZE = 2 * 1024 * 1024  # 2 MB

@app.post("/profile-pic")
async def upload_profile_pic(file: UploadFile = File(...)):
    # 直接讀取全部內容(不建議用於大檔案)
    content = await file.read()
    if len(content) > MAX_SIZE:
        raise HTTPException(
            status_code=413,
            detail="圖片檔案過大,請控制在 2 MB 以內"
        )
    # 實作儲存或後續處理
    with open(f"./uploads/{file.filename}", "wb") as f:
        f.write(content)
    return {"filename": file.filename, "size_kb": len(content) // 1024}

說明

  • 這種方式適合 小於 10 MB 的檔案,因為一次性讀入記憶體。
  • 若檔案過大,await file.read() 仍會先把全部資料讀入,會浪費資源。

範例 3️⃣:流式讀取並即時限制大小(適合大檔案)

# main.py
import aiofiles
from fastapi import FastAPI, Request, HTTPException, UploadFile, File

app = FastAPI()
MAX_SIZE = 10 * 1024 * 1024  # 10 MB

@app.post("/large-upload")
async def upload_large_file(request: Request, file: UploadFile = File(...)):
    # 1. 先取得檔案名稱與 MIME
    filename = file.filename
    size = 0
    # 2. 使用 aiofiles 逐塊寫入磁碟,同時累積大小
    async with aiofiles.open(f"./uploads/{filename}", "wb") as out_file:
        async for chunk in file.stream(1024 * 1024):  # 每次 1 MB
            size += len(chunk)
            if size > MAX_SIZE:
                # 超過上限,立即中斷寫入並刪除已寫入的部份
                await out_file.close()
                import os
                os.remove(f"./uploads/{filename}")
                raise HTTPException(
                    status_code=413,
                    detail=f"檔案大小超過上限 {MAX_SIZE // (1024*1024)} MB"
                )
            await out_file.write(chunk)
    return {"filename": filename, "size_kb": size // 1024}

說明

  • file.stream(chunk_size) 會回傳非同步生成器,每次傳回 chunk_size 位元組。
  • 當累積大小突破限制時,我們立即 刪除已寫入的檔案,避免留下半成品。
  • 這種方式最符合 大檔案上傳 的需求,且不會一次性占用過多記憶體。

範例 4️⃣:自訂 Depends 以在多個路由共用大小檢查

# deps.py
from fastapi import UploadFile, HTTPException, Depends

MAX_SIZE = 3 * 1024 * 1024  # 3 MB

async def verify_file_size(file: UploadFile = Depends()):
    content = await file.read()
    if len(content) > MAX_SIZE:
        raise HTTPException(
            status_code=413,
            detail="檔案超過 3 MB 限制"
        )
    # 重設檔案指標,讓後續路由仍可讀取
    await file.seek(0)
    return file
# main.py
from fastapi import FastAPI, File, UploadFile, Depends
from deps import verify_file_size

app = FastAPI()

@app.post("/doc")
async def upload_doc(file: UploadFile = Depends(verify_file_size)):
    # 此時已確保檔案大小合格
    data = await file.read()
    # 進行儲存或其他處理
    return {"filename": file.filename, "size_kb": len(data)//1024}

說明

  • 利用 Depends 把檢查邏輯抽離,可在多個 endpoint 重複使用,保持程式碼 DRY(Don't Repeat Yourself)。
  • await file.seek(0) 讓檔案指標回到開頭,避免因先前 read() 而導致後續讀取不到內容。

範例 5️⃣:結合 pydantic 驗證檔案類型與大小

# schemas.py
from pydantic import BaseModel, validator

class FileMeta(BaseModel):
    filename: str
    content_type: str
    size: int

    @validator("size")
    def size_must_be_under_limit(cls, v):
        limit = 4 * 1024 * 1024  # 4 MB
        if v > limit:
            raise ValueError(f"檔案大小不可超過 {limit // (1024*1024)} MB")
        return v
# main.py
from fastapi import FastAPI, UploadFile, File, HTTPException
from schemas import FileMeta

app = FastAPI()

@app.post("/validated-upload")
async def validated_upload(file: UploadFile = File(...)):
    content = await file.read()
    try:
        meta = FileMeta(
            filename=file.filename,
            content_type=file.content_type,
            size=len(content)
        )
    except ValueError as e:
        raise HTTPException(status_code=413, detail=str(e))
    # 儲存或其他處理
    return {"meta": meta.dict()}

說明

  • pydantic 的 validator 讓 模型層面 直接負責大小驗證,錯誤訊息更統一。

常見陷阱與最佳實踐

陷阱 說明 解決方式
只檢查 Content‑Length 客戶端可以不傳或偽造此標頭,導致檔案實際大小仍可能超過限制。 同時在 讀取階段 進行實際位元組累計檢查(如範例 3)。
await file.read() 後才檢查大小 已把全部檔案載入記憶體,若檔案超大會造成 OOM(Out‑Of‑Memory)。 使用 流式分塊讀取 的方式,邊讀邊驗。
忘記清理超限檔案 若在寫入磁碟後才發現超過上限,半成品檔案會佔用磁碟空間。 在檢測到超限時 立即刪除 已寫入的檔案(範例 3)。
未設定 max_concurrency 大量同時上傳會造成同時佔用大量記憶體/磁碟 I/O。 uvicorngunicorn 啟動時調整 worker 數量與 --limit-concurrency
錯誤的回傳狀態碼 使用 400、500 讓前端無法辨識「檔案過大」的問題。 返回 413 Payload Too Large 並提供清晰的 detail 訊息。

最佳實踐總結

  1. 先檢查 Header:如果有 Content‑Length,立即回應 413。
  2. 流式驗證:對於可能的大檔案,使用 UploadFile.stream() 逐塊累計大小。
  3. 統一錯誤訊息:使用 HTTPException(status_code=413, detail=…),讓前端 UI 能顯示友善提示。
  4. 設定全域上限:在 uvicorn 啟動參數中加入 --limit-max-request-size(若使用 starletteLimitUploadSizeMiddleware)。
  5. 日誌與監控:將每次被拒絕的上傳事件寫入日誌,並在 Prometheus/Grafana 中監控 upload_rejected_total 指標。

實際應用場景

場景 為什麼需要限制檔案大小 建議實作方式
使用者上傳頭像 圖片通常不超過 2 MB,過大會拖慢頁面載入 路由層 UploadFile.read() + MAX_SIZE = 2 MB
企業文件上傳(PDF、Word) 法務部門可能上傳 10–20 MB 的合約,需防止過大檔案占用磁碟 全域 Middleware + 流式寫入,MAX_SIZE = 25 MB
影片或大檔案備份 單檔可能達到百 MB,需分塊寫入並即時檢查 流式 file.stream() + 逐塊累計,MAX_SIZE = 500 MB
API 平台提供上傳服務 多租戶環境下,每個租戶的上傳上限不同 使用 Depends 注入租戶特定的 MAX_SIZE,在檢查函式中動態取得。
手機 App 上傳相簿 行動裝置頻寬有限,過大檔案會耗盡流量 前端先限制檔案大小,後端再加 Content‑Length 檢查,雙重保護。

總結

  • 限制檔案大小 是保護 FastAPI 應用免於資源耗盡與安全風險的關鍵步驟。
  • 我們可以在 全域 Middleware路由層流式讀取依賴注入(Depends) 多個層面實作檢查,根據實際需求選擇最合適的方式。
  • 切記:僅檢查 Content‑Length 並不足夠,必須在實際讀取或寫入時再次驗證,並在超限時使用 HTTP 413 回應,同時清理已寫入的半成品檔案。
  • 透過 日誌、監控 以及 統一錯誤訊息,你可以快速定位問題並提供良好的使用者體驗。

掌握上述技巧後,你的 FastAPI 服務將在 表單與檔案上傳 的場景中更加 安全、穩定且具彈性。祝開發順利,持續寫出高品質的 API!