Most APIs have a place that accepts files: profile pictures, attached documents, import files. The right way is to declare a parameter of type UploadFile with File(), check content_type and count the size while reading in chunks, then save under a safe server-generated filename. That way large files no longer fill up RAM, wrong-format files get blocked immediately, and a client-supplied filename can never overwrite files outside the upload directory.
The Problem
Say you write an endpoint that accepts a profile picture. The familiar approach is to read the whole file contents and write them straight to disk:
from fastapi import FastAPI, File, UploadFileapp = FastAPI()@app.post("/upload")async def upload(file: UploadFile = File(...)): content = await file.read() # reads the whole file into RAM with open(f"uploads/{file.filename}", "wb") as f: f.write(content) return {"filename": file.filename, "size": len(content)}
This approach has three weaknesses. First, await file.read() loads the entire file into memory — a single 500 MB file is enough to exhaust the server’s RAM with just one request. Second, the endpoint accepts every file type with no format or size check, so a client can send an executable or a huge file. Third, file.filename comes from the client, so an attacker can use a name like ../../.env to overwrite files outside the upload directory.
The Solution
Check the format first, read the file in 1 MB chunks to count its size, and save it under a server-generated name:
import uuidfrom pathlib import Pathfrom fastapi import FastAPI, File, HTTPException, UploadFileapp = 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}
The key is the chunked read loop: RAM only ever holds 1 MB at a time, and a file that exceeds the limit is deleted as soon as the overflow is detected. The filename saved to disk keeps only the original extension, while the name itself is replaced with a uuid, so ../ cannot be smuggled in to escape the upload directory. Note: using File() requires python-multipart to be installed — without it, FastAPI fails at startup.
Verified with TestClient: a 100 KB PNG returns 200 with the correct saved filename; an .exe file returns 400; a 6 MB PDF returns 413 and leaves no file behind in the uploads directory.
Apply It to Your Project
This pattern works for any file-accepting endpoint: profile pictures, attached documents, CSV import files. Just adjust the ALLOWED_TYPES set and MAX_SIZE for each type — for example 5 MB for images, 20 MB for documents. To accept several files at once, declare a parameter of type list[UploadFile] and loop over each file with the same validation logic.
Related Posts
- FastAPI: Receive Form Data with Form() — a Standard OAuth2 Login Endpoint in 5 Minutes
- FastAPI: Validate Path Parameters with Path() — Block Bad IDs at the API Gateway in 5 Minutes
- FastAPI: Use Enums for Path Parameters — Endpoints That Only Accept Valid Values in 5 Minutes
Frequently Asked Questions
How is UploadFile different from receiving bytes directly?
Declaring file: bytes = File(...) makes FastAPI read the entire file into RAM before the endpoint runs. UploadFile uses a temporary file on disk (a spooled file) and reads in chunks, so it stays safe with large files — that is why you should use UploadFile for every endpoint that receives real files.
Is checking content_type safe enough?
Not on its own if security requirements are high, because content_type is sent by the client and can be faked. For images, also check the first few bytes of the file (magic bytes) with a library like python-magic, or try opening it with Pillow to be sure it really is a valid image.
With chunked reading, how do I know the total size in advance?
You do not need to know it in advance: just read and keep a running total, then stop and delete the partial file once the limit is exceeded. This also blocks clients that declare a small size but send a large file, because the counted number is the real number of bytes actually read.
Which extra package do I need to use File()?
You need python-multipart, because FastAPI uses it to parse form-data containing files. Install it with pip install python-multipart and remember to add it to your project’s requirements — without this package the server errors out at startup.
💬 Have you tried this in your own project? If you hit an error or know a shorter way, leave a comment below — I read them all and will add the best ideas to the post.
References
- FastAPI — Request Files: https://fastapi.tiangolo.com/tutorial/request-files/
- FastAPI — Request Forms and Files: https://fastapi.tiangolo.com/tutorial/request-forms-and-files/
- Starlette — UploadFile: https://www.starlette.io/requests/
