Trả StreamingResponse với một generator đọc dữ liệu theo lô, thay vì dựng cả file trong bộ nhớ rồi trả một lần.
from fastapi.responses import StreamingResponse
async def rows_csv(db):
yield "id,email,created_at\n"
async for chunk in db.stream_scalars(select(User).execution_options(yield_per=1000)):
yield f"{chunk.id},{chunk.email},{chunk.created_at}\n"
@app.get("/export/users")
async def export_users(db: DbSession):
return StreamingResponse(
rows_csv(db),
media_type="text/csv",
headers={"Content-Disposition": 'attachment; filename="users.csv"'},
)Bốn điểm hay sai khi triển khai:
- Vòng đời session: generator chạy sau khi handler trả về. Nếu session được đóng bởi dependency yield, con trỏ sẽ chết giữa chừng. Mở session riêng bên trong generator hoặc dùng cursor phía server tự quản lý.
- Không có Content-Length nên client thấy tiến trình dạng chunked, không có phần trăm. Cần progress bar thì phải chuyển sang sinh file nền rồi trả link tải.
- Middleware kiểu BaseHTTPMiddleware (kể cả GZip đặt sai chỗ) có thể gom toàn bộ body vào bộ nhớ, xoá sạch lợi ích của streaming.
- Generator đồng bộ (def có yield) vẫn được hỗ trợ và chạy trong threadpool — dùng nó nếu driver DB là loại đồng bộ, đừng trộn lời gọi blocking vào generator async.