👺
FastAPIで重いファイル生成を安全にダウンロードさせる
FastAPIでCSVなどのファイルを生成してダウンロードさせるとき、実装次第でタイムアウトやメモリ枯渇が起きる。
StreamingResponseを使っていても実装次第で普通にタイムアウトする。
レスポンス開始が「CSV生成後」になる実装
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import csv
import io
app = FastAPI()
@app.get("/csv")
def download_csv():
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["id", "name", "comment"])
for i in range(1_000_000):
writer.writerow([i, "user", 'hello, "world"\n'])
buf.seek(0)
return StreamingResponse(
buf,
media_type="text/csv",
headers={
"Content-Disposition": "attachment; filename=users.csv"
}
)
この実装の特徴
- CSVをすべて生成し終えてからレスポンスが始まる
- StreamingResponseを使っていても実質バッファリング
- 生成が重いと timeout しやすい
レスポンス開始が「CSV生成途中」になる実装
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import csv
import io
app = FastAPI()
@app.get("/csv")
def download_csv():
def iter_csv():
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["id", "name", "comment"])
yield buf.getvalue()
buf.seek(0)
buf.truncate(0)
for i in range(1_000_000):
writer.writerow([i, "user", 'hello, "world"\n'])
yield buf.getvalue()
buf.seek(0)
buf.truncate(0)
return StreamingResponse(
iter_csv(),
media_type="text/csv",
headers={
"Content-Disposition": "attachment; filename=users.csv"
}
)
この実装の特徴
- 1行生成するたびにレスポンスが進む
- レスポンスがすぐに開始される
- メモリ使用量が小さい
- 大きなCSVでも安定しやすい
実装を分けているポイント
- StreamingResponseかどうかではなく「いつ yield されるか」
- 「全部生成 → 返す」と「生成しながら返す」の違い
- 重い処理ほど後者が効く
Discussion