0Pricing
FastAPI Backend Development Bootcamp · 课时

多部分上传与内容验证

接收 UploadFile 输入,验证 MIME 类型和大小限制,并防范恶意载荷。

多部分上传与内容验证 是 CoddyKit 上的免费 FastAPI Backend Development Bootcamp 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 FastAPI Backend Development Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why Multipart Uploads Matter

Regular JSON request bodies cannot carry raw binary files efficiently. To upload an image, PDF, or video, browsers send a multipart/form-data request, which packs each field (text values and file bytes) into separate parts with their own headers.

FastAPI exposes incoming files through two helpers:

  • UploadFile — a spooled file object that keeps small files in memory and large files on disk automatically.
  • File() — a parameter marker that tells FastAPI to read this value from the multipart body.

In this lesson you will accept uploads, validate their MIME type and size, and reject malicious or oversized payloads before they touch your storage.

Your First UploadFile Endpoint

An UploadFile parameter gives you the original filename, the declared content_type, and async methods like read() and seek(). Always declare it with = File(...) so FastAPI parses it from the multipart body.

Note the handler is async because file I/O on UploadFile is awaitable.

from fastapi import FastAPI, UploadFile, File

app = FastAPI()

@app.post("/upload")
async def upload(file: UploadFile = File(...)):
    contents = await file.read()
    return {
        "filename": file.filename,
        "content_type": file.content_type,
        "size_bytes": len(contents),
    }

Never Trust the Declared content_type

The content_type on an UploadFile comes straight from the client. An attacker can label a .exe as image/png. Use it as a cheap first filter, but never as your only check.

A robust pipeline does three things, in order:

  • Reject obviously wrong declared types quickly (cheap).
  • Enforce a hard size limit while streaming (prevents memory exhaustion).
  • Inspect the real file bytes (magic numbers) to confirm the true type.

The next scenes build each layer.

Allowlisting MIME Types

Always use an allowlist, never a blocklist. List exactly the types you support and reject everything else. Return 415 Unsupported Media Type when the declared type is not allowed.

Keep the set small and explicit so new file formats are an intentional decision, not an accident.

from fastapi import FastAPI, UploadFile, File, HTTPException

app = FastAPI()

ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}

@app.post("/documents")
async def create_document(file: UploadFile = File(...)):
    if file.content_type not in ALLOWED_TYPES:
        raise HTTPException(
            status_code=415,
            detail=f"Unsupported type: {file.content_type}",
        )
    return {"ok": True, "filename": file.filename}

Enforcing a Size Limit by Streaming

Calling await file.read() loads the entire file into memory. A 2 GB upload could crash your worker. Instead, read in fixed-size chunks and abort the moment the running total exceeds your limit.

Return 413 Request Entity Too Large when the cap is breached. This keeps memory bounded no matter how big the client claims the file is.

from fastapi import FastAPI, UploadFile, File, HTTPException

app = FastAPI()

MAX_SIZE = 5 * 1024 * 1024  # 5 MB
CHUNK = 1024 * 1024         # 1 MB

@app.post("/upload")
async def upload(file: UploadFile = File(...)):
    total = 0
    while chunk := await file.read(CHUNK):
        total += len(chunk)
        if total > MAX_SIZE:
            raise HTTPException(413, "File too large")
    return {"filename": file.filename, "size": total}

Streaming Straight to Disk Safely

Once size and type pass, stream the chunks to a destination file instead of holding them in memory. Combine the size check with the write loop so you stop early on oversized payloads and never persist a partial-but-huge file.

Use await file.seek(0) if you read the stream earlier and need to start over.

import aiofiles
from fastapi import UploadFile, File, HTTPException

MAX_SIZE = 5 * 1024 * 1024

async def save_upload(file: UploadFile, dest: str) -> int:
    total = 0
    async with aiofiles.open(dest, "wb") as out:
        while chunk := await file.read(1024 * 1024):
            total += len(chunk)
            if total > MAX_SIZE:
                raise HTTPException(413, "File too large")
            await out.write(chunk)
    return total

Verifying Real Content with Magic Numbers

The most reliable type check inspects the file's leading bytes, its magic number. A PNG always starts with \x89PNG\r\n\x1a\n; a JPEG with \xff\xd8\xff; a PDF with %PDF.

This pure-Python function maps a byte prefix to a real MIME type. You can run it on an online judge with no framework at all.

def sniff_mime(head: bytes) -> str | None:
    signatures = {
        b"\x89PNG\r\n\x1a\n": "image/png",
        b"\xff\xd8\xff": "image/jpeg",
        b"%PDF": "application/pdf",
    }
    for magic, mime in signatures.items():
        if head.startswith(magic):
            return mime
    return None


if __name__ == "__main__":
    print(sniff_mime(b"\x89PNG\r\n\x1a\nrest"))  # image/png
    print(sniff_mime(b"%PDF-1.7"))               # application/pdf
    print(sniff_mime(b"MZ\x90\x00"))             # None (rejected)

Cross-Checking Declared vs Real Type

Combine the layers: read just enough bytes to sniff the magic number, confirm it is in your allowlist, and verify it matches what the client declared. A mismatch (declared image/png but real bytes say PDF) is a strong signal of a malicious or buggy client, so reject it.

After sniffing, call await file.seek(0) so the full file can still be saved.

from fastapi import UploadFile, File, HTTPException

ALLOWED = {"image/png", "image/jpeg", "application/pdf"}

async def validate_type(file: UploadFile) -> str:
    head = await file.read(8)
    await file.seek(0)
    real = sniff_mime(head)
    if real not in ALLOWED:
        raise HTTPException(415, "Content not allowed")
    if file.content_type != real:
        raise HTTPException(415, "Declared type mismatch")
    return real

Sanitizing Filenames

Never use the client-supplied filename directly as a storage path. Names like ../../etc/passwd enable path-traversal, and odd characters break filesystems. Strip the directory part, keep a safe character set, and prefer a generated name plus a validated extension.

This helper is pure Python and judge-runnable.

import re
import uuid
from pathlib import PurePosixPath

EXT_FOR = {"image/png": ".png", "image/jpeg": ".jpg", "application/pdf": ".pdf"}

def safe_name(original: str, mime: str) -> str:
    base = PurePosixPath(original).name          # drop any path parts
    base = re.sub(r"[^A-Za-z0-9._-]", "_", base)  # keep safe chars
    ext = EXT_FOR.get(mime, "")
    return f"{uuid.uuid4().hex}{ext}"


if __name__ == "__main__":
    print(safe_name("../../etc/passwd", "image/png").endswith(".png"))
    print("/" not in safe_name("weird name!.jpg", "image/jpeg"))

Handling Multiple Files at Once

To accept several files in one request, declare the parameter as a list[UploadFile]. The client sends the same form field name repeatedly. Validate each file independently and fail the whole request if any one is invalid, so partial uploads never leave inconsistent state.

from fastapi import FastAPI, UploadFile, File, HTTPException

app = FastAPI()
ALLOWED = {"image/png", "image/jpeg"}

@app.post("/gallery")
async def gallery(files: list[UploadFile] = File(...)):
    if len(files) > 10:
        raise HTTPException(400, "Too many files (max 10)")
    for f in files:
        if f.content_type not in ALLOWED:
            raise HTTPException(415, f"{f.filename}: bad type")
    return {"received": [f.filename for f in files]}

Packaging Validation as a Dependency

Repeating type and size checks in every endpoint is error-prone. Wrap them in a reusable FastAPI dependency. The dependency runs the full pipeline and returns a clean, validated UploadFile, so your route stays focused on business logic.

This is the production-grade shape: allowlist, streamed size cap, magic-number sniff, and filename safety all in one place.

from fastapi import Depends, UploadFile, File, HTTPException

MAX_SIZE = 5 * 1024 * 1024

async def validated_upload(file: UploadFile = File(...)) -> UploadFile:
    head = await file.read(8)
    if sniff_mime(head) not in {"image/png", "image/jpeg", "application/pdf"}:
        raise HTTPException(415, "Unsupported content")
    total = len(head)
    while chunk := await file.read(1024 * 1024):
        total += len(chunk)
        if total > MAX_SIZE:
            raise HTTPException(413, "File too large")
    await file.seek(0)
    return file

@app.post("/secure-upload")
async def secure_upload(file: UploadFile = Depends(validated_upload)):
    return {"filename": file.filename}

Quick Check: Choosing the Right Guard

An endpoint accepts profile pictures. A user uploads a 3 GB file whose content_type header claims image/png, but the bytes are actually an executable. Which single combination of checks reliably protects the server?

Recap: A Layered Upload Defense

You built a complete, defensive upload pipeline for FastAPI:

  • UploadFile + File() accept multipart data with streaming-friendly I/O.
  • Allowlist the declared MIME type and return 415 for anything unexpected, but never trust that header alone.
  • Stream in chunks and abort with 413 once a size cap is exceeded, keeping memory bounded.
  • Sniff magic numbers to confirm the real content type and reject declared-vs-real mismatches.
  • Sanitize filenames with generated names to stop path traversal.
  • Package it as a dependency so every endpoint reuses the same guard.

Layered checks, ordered cheap-to-expensive, give you robust protection against oversized and malicious uploads.

常见问题解答

「多部分上传与内容验证」课时是免费的吗?

是的 — 「多部分上传与内容验证」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 FastAPI Backend Development Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 FastAPI Backend Development Bootcamp 课程共包含 4 节课。

「多部分上传与内容验证」这节课中我会学到什么?

接收 UploadFile 输入,验证 MIME 类型和大小限制,并防范恶意载荷。 你通过在浏览器中直接运行的动手代码来练习 FastAPI Backend Development Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 FastAPI Backend Development Bootcamp 需要有经验吗?

无需任何先前经验。CoddyKit 上的 FastAPI Backend Development Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「多部分上传与内容验证」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 FastAPI Backend Development Bootcamp 课中编写并运行代码吗?

能。每节 FastAPI Backend Development Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 多部分上传与内容验证
  2. 流式响应与范围请求
  3. 将存储卸载到兼容 S3 的存储桶
  4. 异步图像与文档转换
← 返回 FastAPI Backend Development Bootcamp