← Back to FastAPI map
FastAPI · HTTP layer

Files & forms

Receiving uploaded files, checking them before you keep them, and reading normal form fields.

multipart

Overview

HTML forms and file uploads are not JSON: they arrive as application/x-www-form-urlencoded or multipart/form-data. Form() and File() read those encodings once python-multipart is installed. UploadFile holds the upload in a spooled temporary file - in memory while small, moved to disk when large - so big uploads do not have to fit in RAM.

Key concepts

UploadFile vs bytes
Typing a parameter as bytes loads the whole file into memory. UploadFile spools to disk and exposes filename, content_type, and an async read().
Form()
Reads individual form fields. OAuth2PasswordRequestForm, used for login, is built on it.
Multipart
One multipart request can carry form fields and files together.
One body encoding
A request body is either form data or JSON, so a route cannot take both form fields and a JSON model.

Best practices

  • Never trust filename or content_type - generate your own storage names and check the actual file contents.
  • Enforce a size limit; await file.read() still pulls the entire file into memory.
  • For large files, write to disk or object storage in chunks, or let clients upload straight to storage with presigned URLs.

File uploads and form data

UploadFile - single and multiple

Files
# pip install python-multipart (required for File/Form) from fastapi import File, UploadFile, HTTPException from typing import List import shutil, os # UploadFile - preferred for all uploads @app.post("/upload") async def upload(file: UploadFile): contents = await file.read() return { "filename": file.filename, "content_type": file.content_type, "size_bytes": len(contents) } # Save to disk @app.post("/upload-save") async def save_file(file: UploadFile): os.makedirs("uploads", exist_ok=True) with open(f"uploads/{file.filename}", "wb") as f: shutil.copyfileobj(file.file, f) return {"saved": file.filename} # Multiple files @app.post("/upload-many") async def upload_many(files: List[UploadFile]): return [{"name": f.filename, "type": f.content_type} for f in files] # Validate file type and size @app.post("/upload-image") async def upload_image(file: UploadFile): if file.content_type not in ["image/jpeg", "image/png"]: raise HTTPException(400, "Only JPEG and PNG allowed") contents = await file.read() if len(contents) > 5 * 1024 * 1024: raise HTTPException(413, "File exceeds 5MB limit") return {"ok": True}

Watch out: file.filename comes from the client. Saving to f"uploads/{file.filename}" as-is lets a name like "../main.py" escape the folder - generate your own name, or use os.path.basename and check the result.

Tip: content_type is also client-supplied. For anything security-sensitive, check the file bytes themselves rather than trusting the header.

Form fields + file together

Forms
from fastapi import Form, File, UploadFile from typing import Optional # HTML form data (application/x-www-form-urlencoded) @app.post("/login") async def form_login( username: str = Form(...), password: str = Form(...) ): return {"username": username} # Form fields + file (multipart/form-data) @app.post("/update-profile") async def update_profile( name: str = Form(...), bio: Optional[str] = Form(None), avatar: Optional[UploadFile] = File(None) ): return {"name": name, "has_avatar": avatar is not None}

Watch out: You cannot mix Form/File with a JSON body in the same route - the request is either multipart/form data or JSON, so pick one.

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...