本文 AI 產出,尚未審核
FastAPI – 表單與檔案上傳(Form & File Upload)
主題:限制檔案大小
簡介
在 Web 應用中,表單與檔案上傳是最常見的互動方式之一。若未對上傳檔案的大小進行適當限制,可能會導致 伺服器資源耗盡、磁碟空間被填滿,甚至成為 DoS(Denial‑of‑Service) 攻擊的入口。FastAPI 內建了與 Starlette 整合的請求處理機制,讓我們可以輕鬆地在路由層或全域層面加入檔案大小驗證。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你完成 安全、彈性的檔案大小限制,讓你的 API 在上傳功能上更可靠。
核心概念
1. 為什麼要在 FastAPI 中限制檔案大小?
| 風險 | 可能的影響 |
|---|---|
| 資源耗盡 | 大檔案會佔用大量記憶體與磁碟,導致其他請求變慢或失敗 |
| 安全漏洞 | 攻擊者可利用無限制上傳執行 DoS 或儲存惡意檔案 |
| 使用者體驗 | 使用者若上傳過大的檔案,往往會等很久才收到錯誤回應,影響滿意度 |
因此,我們需要在 接收檔案前 就先檢查大小,或在 流式讀取時 立即中斷過大的傳輸。
2. FastAPI 處理檔案的兩種方式
一次性載入(
UploadFile)- FastAPI 會先把檔案暫存於磁碟(或記憶體),然後把
UploadFile物件傳給 endpoint。 - 適合小檔案或需要直接存取檔案內容的情境。
- FastAPI 會先把檔案暫存於磁碟(或記憶體),然後把
流式讀取(
StreamingFormDataParser)- 透過
request.stream()逐塊讀取資料,能在讀取過程中即時檢查大小。 - 適合大檔案或需要「邊讀邊驗」的需求。
- 透過
以下範例會同時展示這兩種方式的 檔案大小限制。
3. 兩種實作策略
| 策略 | 說明 | 適用情境 |
|---|---|---|
| 全域 Middleware | 在請求進入路由前攔截,檢查 Content‑Length 或實際讀取的位元組數 |
所有上傳皆需統一上限 |
| 路由層驗證 | 在特定 endpoint 內自行檢查檔案大小 | 部分上傳有不同上限或需要客製化錯誤訊息 |
程式碼範例
以下程式碼均以 Python 3.11、FastAPI 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。 | 在 uvicorn 或 gunicorn 啟動時調整 worker 數量與 --limit-concurrency。 |
| 錯誤的回傳狀態碼 | 使用 400、500 讓前端無法辨識「檔案過大」的問題。 | 返回 413 Payload Too Large 並提供清晰的 detail 訊息。 |
最佳實踐總結
- 先檢查 Header:如果有
Content‑Length,立即回應 413。 - 流式驗證:對於可能的大檔案,使用
UploadFile.stream()逐塊累計大小。 - 統一錯誤訊息:使用
HTTPException(status_code=413, detail=…),讓前端 UI 能顯示友善提示。 - 設定全域上限:在
uvicorn啟動參數中加入--limit-max-request-size(若使用starlette的LimitUploadSizeMiddleware)。 - 日誌與監控:將每次被拒絕的上傳事件寫入日誌,並在 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!