Hầu hết API đều có chỗ nhận file: ảnh đại diện, tài liệu đính kèm, file import. Cách đúng là khai báo tham số kiểu UploadFile với File(), kiểm tra content_type và đếm dung lượng trong lúc đọc từng chunk, rồi lưu bằng tên file an toàn do server tự sinh. Làm vậy file lớn không còn làm đầy RAM, file sai định dạng bị chặn ngay, và tên file do client gửi không thể ghi đè ra ngoài thư mục upload.

Vấn đề

Giả sử bạn viết endpoint nhận ảnh đại diện. Cách quen thuộc là đọc toàn bộ nội dung file rồi ghi thẳng xuống đĩa:

from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/upload")
async def upload(file: UploadFile = File(...)):
content = await file.read() # đọc toàn bộ file vào RAM
with open(f"uploads/{file.filename}", "wb") as f:
f.write(content)
return {"filename": file.filename, "size": len(content)}

Cách này có ba điểm yếu. Thứ nhất, await file.read() nạp toàn bộ file vào bộ nhớ — một file 500 MB đủ làm server hết RAM dù chỉ có một request. Thứ hai, endpoint nhận mọi loại file, không kiểm tra định dạng hay dung lượng, nên client có thể gửi file thực thi hoặc file khổng lồ. Thứ ba, file.filename do client gửi, kẻ xấu có thể đặt tên như ../../.env để ghi đè file ngoài thư mục upload.

Giải pháp

Kiểm tra định dạng trước, đọc file theo từng chunk 1 MB để đếm dung lượng, và lưu bằng tên do server tự sinh:

import uuid
from pathlib import Path
from fastapi import FastAPI, File, HTTPException, UploadFile
app = FastAPI()
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}
MAX_SIZE = 5 * 1024 * 1024 # 5 MB
@app.post("/upload")
async def upload(file: UploadFile = File(...)):
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(status_code=400, detail="Định dạng file không được hỗ trợ")
safe_name = f"{uuid.uuid4().hex}{Path(file.filename).suffix}"
target = UPLOAD_DIR / safe_name
size = 0
with target.open("wb") as buffer:
while chunk := await file.read(1024 * 1024):
size += len(chunk)
if size > MAX_SIZE:
break
buffer.write(chunk)
if size > MAX_SIZE:
target.unlink(missing_ok=True)
raise HTTPException(status_code=413, detail="File vượt quá 5 MB")
return {"filename": safe_name, "size": size}

Điểm mấu chốt là vòng lặp đọc từng chunk: RAM lúc nào cũng chỉ giữ 1 MB, và file vượt giới hạn bị xóa ngay sau khi phát hiện. Tên file lưu xuống đĩa chỉ giữ lại phần mở rộng gốc, phần tên được thay bằng uuid nên không thể chèn ../ để thoát khỏi thư mục upload. Lưu ý: dùng File() bắt buộc cài python-multipart, nếu thiếu FastAPI báo lỗi ngay khi khởi động.

Kiểm chứng bằng TestClient: gửi file PNG 100 KB trả 200 kèm đúng tên file đã lưu; gửi file .exe trả 400; gửi file PDF 6 MB trả 413 và không còn file nào trong thư mục uploads.

Áp dụng vào dự án của bạn

Mẫu này dùng được cho mọi endpoint nhận file: ảnh đại diện, tài liệu đính kèm, file CSV import. Chỉ cần đổi tập ALLOWED_TYPES và MAX_SIZE theo từng loại, ví dụ ảnh 5 MB, tài liệu 20 MB. Nếu cần nhận nhiều file cùng lúc, khai báo tham số kiểu list[UploadFile] và lặp qua từng file với cùng logic kiểm tra.

Bài viết liên quan

Câu hỏi thường gặp

UploadFile khác gì nhận bytes trực tiếp?

Khai báo file: bytes = File(...) khiến FastAPI đọc toàn bộ file vào RAM trước khi endpoint chạy. UploadFile dùng file tạm trên đĩa (spooled file), đọc từng chunk nên an toàn với file lớn — đó là lý do nên dùng UploadFile cho mọi endpoint nhận file thật.

Kiểm tra content_type có đủ an toàn không?

Chưa đủ nếu yêu cầu bảo mật cao, vì content_type do client gửi và có thể giả mạo. Với ảnh, kiểm tra thêm vài byte đầu file (magic bytes) bằng thư viện như python-magic, hoặc mở thử bằng Pillow để chắc chắn đó đúng là ảnh hợp lệ.

Đọc file theo chunk thì sao biết tổng dung lượng trước?

Không cần biết trước: cứ đọc và cộng dồn, vượt giới hạn thì dừng và xóa file dở. Cách này còn chặn được client khai dung lượng nhỏ nhưng gửi file lớn, vì con số đếm được là số byte thật đã đọc.

Cần cài thêm gói nào để dùng File()?

Cần python-multipart, vì FastAPI dùng nó để parse form-data chứa file. Cài bằng pip install python-multipart, và nhớ thêm vào requirements của dự án — thiếu gói này server báo lỗi ngay khi khởi động.

💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.

Nguồn tham khảo