本文 AI 產出,尚未審核
FastAPI 教學:請求與回應 – RedirectResponse、FileResponse、StreamingResponse
簡介
在 Web 開發中,回應(Response) 不僅僅是傳回 JSON 資料那麼簡單。
有時候我們需要將使用者導向另一個 URL(Redirect),或是直接下載檔案(File),甚至把大型檔案或即時串流資料以 流式 方式傳送給前端。FastAPI 內建了三個非常好用的回應類別:
RedirectResponse– 讓瀏覽器自動跳轉到指定網址FileResponse– 直接回傳本機檔案,支援Content‑Disposition、範圍請求(range)等StreamingResponse– 以 generator 或 async generator 方式逐塊傳送資料,適合大檔案、影片、即時資料等情境
掌握這三個回應類別,能讓你的 API 更具彈性,也能提升使用者體驗與效能。本文將從概念說明、實作範例、常見陷阱與最佳實踐,最後以實務情境帶你快速上手。
核心概念
1. RedirectResponse
RedirectResponse 繼承自 Response,會在 HTTP 標頭加入 Location,並預設使用 302 Found(臨時重新導向)。如果要做永久導向(301)或其他狀態碼,只要在建構子裡自行指定即可。
為什麼需要 Redirect?
- 使用者完成登入後,導向原本想去的頁面
- API 變更路徑,舊網址仍然保留,以免斷鏈
- OAuth2、第三方授權流程中,常會需要跳轉回 callback URL
基本範例
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.get("/old-path")
def old_path():
"""
舊的 API 路徑,現在改到 /new-path。
使用 301 永久導向,讓搜尋引擎與瀏覽器快取新位置。
"""
return RedirectResponse(url="/new-path", status_code=301)
@app.get("/new-path")
def new_path():
return {"message": "已成功導向至新路徑!"}
小技巧:若要保留原始查詢參數,可使用
request.url_for並手動拼接query_params。
2. FileResponse
FileResponse 用於 直接回傳檔案,它會自動設定 Content-Type、Content-Length,並支援 斷點續傳(Range)與 下載檔名(Content-Disposition)的設定。非常適合提供 PDF、圖片、CSV、ZIP 等檔案下載功能。
重要參數說明
| 參數 | 說明 |
|---|---|
path |
檔案在伺服器的絕對或相對路徑 |
media_type |
回傳的 MIME type,預設會根據檔案副檔名偵測 |
filename |
若提供,會在 Content-Disposition 中加入 attachment; filename="...",讓瀏覽器下載而不是直接開啟 |
headers |
其他自訂標頭,例如 Cache-Control |
background |
完成回傳後要執行的背景任務(例如刪除暫存檔) |
基本範例:提供下載 PDF
from fastapi import FastAPI
from fastapi.responses import FileResponse
import pathlib
app = FastAPI()
BASE_DIR = pathlib.Path(__file__).parent
@app.get("/download/report")
def download_report():
"""
下載年度報告(PDF),檔名會顯示為 report_2024.pdf
"""
file_path = BASE_DIR / "static" / "report_2024.pdf"
return FileResponse(
path=str(file_path),
media_type="application/pdf",
filename="report_2024.pdf",
headers={"Cache-Control": "no-cache"},
)
範例:支援斷點續傳的大檔案
@app.get("/download/large-video")
def download_video():
"""
大型影片檔案,讓瀏覽器可以使用「快取」或「續傳」功能。
"""
video_path = BASE_DIR / "videos" / "movie_4k.mp4"
return FileResponse(
path=str(video_path),
media_type="video/mp4",
filename="movie_4k.mp4",
# FastAPI 內建支援 Range,無需額外設定
)
3. StreamingResponse
StreamingResponse 允許 逐塊(chunk)傳送 資料,適用於:
- 大檔案的即時下載(不想一次載入記憶體)
- 影片/音訊串流(HLS、MPEG‑DASH)
- 產生 CSV、Excel 等動態檔案
- 伺服器端事件(Server‑Sent Events, SSE)
兩種常見產生方式
- 同步 generator:適合 CPU‑bound、簡單的資料產生
- 非同步 async generator:適合 I/O‑bound(如讀檔、呼叫外部 API)
範例 1:同步 generator 產生 CSV
import csv
from io import StringIO
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
def csv_generator():
"""
產生 1000 行測試資料的 CSV,每次 yield 一行字串。
"""
buffer = StringIO()
writer = csv.writer(buffer)
writer.writerow(["id", "name", "score"])
yield buffer.getvalue()
buffer.seek(0)
buffer.truncate(0) # 清空緩衝區
for i in range(1, 1001):
writer.writerow([i, f"User{i}", i % 100])
yield buffer.getvalue()
buffer.seek(0)
buffer.truncate(0)
@app.get("/stream/csv")
def stream_csv():
"""
透過 StreamingResponse 直接把 CSV 串流回給前端,
瀏覽器會自動觸發下載。
"""
return StreamingResponse(
csv_generator(),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=users.csv"},
)
範例 2:非同步 generator 讀取大檔案
import aiofiles
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def file_chunk_reader(file_path: str, chunk_size: int = 1024 * 1024):
"""
非同步讀取檔案,每次傳回 chunk_size 大小的位元組。
"""
async with aiofiles.open(file_path, mode="rb") as f:
while True:
chunk = await f.read(chunk_size)
if not chunk:
break
yield chunk
@app.get("/stream/large-log")
async def stream_large_log():
"""
讓使用者即時下載巨大的 log 檔(例如 5 GB),
只佔用極少的記憶體。
"""
log_path = "logs/2024-11-20.log"
return StreamingResponse(
file_chunk_reader(log_path),
media_type="text/plain",
headers={"Content-Disposition": "attachment; filename=2024-11-20.log"},
)
範例 3:Server‑Sent Events(SSE)實作
import asyncio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def event_generator():
"""
每隔 2 秒推送一次訊息給前端,適合作為即時監控或聊天室。
"""
counter = 0
while True:
await asyncio.sleep(2)
counter += 1
yield f"data: 訊息 {counter}\\n\\n"
@app.get("/sse/heartbeat")
def sse_heartbeat():
"""
SSE 端點,前端可以使用 EventSource 連結。
"""
return StreamingResponse(event_generator(), media_type="text/event-stream")
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 最佳實踐 |
|---|---|---|
忘記設定 filename |
FileResponse 若未提供 filename,瀏覽器會直接在網址列顯示檔案路徑,使用者體驗差。 |
使用 filename= 參數,或自行在 headers 加 Content‑Disposition。 |
| 大檔案一次載入記憶體 | 直接回傳 Response(content=bytes) 會把整個檔案讀入記憶體,容易 OOM。 |
改用 FileResponse(內建分段傳輸)或 StreamingResponse 搭配 generator。 |
| Range 請求未支援 | 若自行實作檔案串流,忘記處理 Range 標頭會導致斷點續傳失效。 |
FileResponse 自動支援;若自行寫 StreamingResponse,要自行解析 Range。 |
| 同步 I/O 阻塞事件迴圈 | 在 async 路由裡使用同步 open()、read() 會阻塞事件迴圈。 |
使用 aiofiles 或其他非同步 I/O 庫;或把阻塞操作搬到執行緒池(run_in_threadpool)。 |
忘記設定 Cache-Control |
靜態檔案或下載檔案若未設定快取策略,會造成不必要的重複下載。 | 依需求加入 Cache-Control: public, max-age=86400 或 no-cache。 |
| 未處理例外 | 檔案不存在或權限不足時會拋出 FileNotFoundError,回傳 500。 |
使用 try/except 包住回傳,或在 FastAPI 中加入自訂例外處理器。 |
最佳實踐小結:
- 盡量使用內建 Response(
FileResponse、RedirectResponse)來減少自行處理 HTTP 標頭的錯誤機率。 - 大檔案或即時串流:優先考慮
StreamingResponse+ async generator。 - 安全性:永遠驗證使用者是否有權限讀取或下載檔案,避免路徑穿越(path traversal)攻擊。
- 測試:使用
TestClient(fastapi.testclient.TestClient)驗證Location、Content‑Disposition、Range等標頭是否正確。
實際應用場景
| 場景 | 使用哪個 Response | 為什麼適合 |
|---|---|---|
| 使用者登入成功後導回原本頁面 | RedirectResponse(302/301) |
簡潔且符合 HTTP 標準,前端不需額外處理 |
| 產出報表(PDF、Excel)讓使用者下載 | FileResponse |
自動偵測 MIME、支援斷點續傳,省去手動寫標頭 |
| 大型影片串流(支援快轉、暫停) | FileResponse + Range |
影片播放器會自行發送 Range,FastAPI 內建支援 |
| 即時產生 CSV(依照使用者條件) | StreamingResponse(同步 generator) |
只在產生時寫入,不佔用大量記憶體 |
| 監控系統即時推送訊息給前端儀表板 | StreamingResponse(SSE) |
低延遲、瀏覽器原生支援 EventSource |
| 大型日志檔案供客服下載分析 | StreamingResponse(async generator) |
直接從磁碟分塊讀取,避免 OOM,且支援斷點續傳(自行實作) |
總結
RedirectResponse:簡化導向流程,記得根據需求選擇 301(永久)或 302(臨時)狀態碼。FileResponse:下載靜態檔案或大檔案的首選,內建 MIME 判斷、斷點續傳與快取控制。StreamingResponse:處理動態產生或巨量資料時的利器,配合同步或非同步 generator,可實作 CSV、影片串流、SSE 等多種應用。
掌握這三種回應類別,你就能在 FastAPI 中自由組合 導向、下載、即時串流 三大功能,寫出既高效又符合使用者期待的 API。趕快把範例搬到自己的專案中,體驗從「回傳 JSON」到「完整 Web 互動」的升級吧!祝開發順利 🎉