Загрузка файла в FastAPI: как читать порциями и ограничить размер

Endpoint загружает файл через await file.read() и получает всё содержимое одним объектом bytes. На маленьком примере это удобно, но большой файл занимает память целиком. Даже если клиент прислал небольшой Content-Length, полезно ограничить количество байтов, которое ваша обработка действительно принимает.

UploadFile уже представляет принятый multipart-файл

FastAPI разбирает multipart и передаёт UploadFile с файловым объектом, который может использовать временное хранение. Чтение порциями ограничивает дополнительную память вашего обработчика. Оно не отменяет факт предварительного приёма multipart парсером. Для ограничения входящего тела до парсинга нужен соответствующий предел сервера, прокси или middleware, который считает поток до маршрута.

В примере допустимы десять байтов, чтобы граничные проверки были видны. Обработчик считает реально прочитанное содержимое и прекращает свою обработку с 413, когда лимит превышен. Не доверяем одному filename: это имя задаёт клиент. Если сохраняете файл, генерируйте безопасное имя и выбирайте каталог сами.

Проверим границу и вычислим хеш

Установите fastapi, httpx и python-multipart. Хеш вычисляется по прочитанным порциям и подтверждает, что обработчик действительно обработал байты файла. Тест отправляет ровно допустимый размер, один лишний байт и пустой файл.

Сохраните весь блок в файл и запустите Python 3.11+ в отдельном окружении с пакетами fastapi, httpx, python-multipart. Для моделей используется Pydantic v2; отдельный сервер для TestClient не нужен.

import hashlib
from fastapi import FastAPI, UploadFile, HTTPException
from fastapi.testclient import TestClient

app = FastAPI()
LIMIT = 10
@app.post("/upload")
async def upload(file: UploadFile):
    size = 0
    digest = hashlib.sha256()
    try:
        while chunk := await file.read(4):
            size += len(chunk)
            if size > LIMIT:
                raise HTTPException(413, "file is too large")
            digest.update(chunk)
        return {"size": size, "sha256": digest.hexdigest()}
    finally:
        await file.close()

with TestClient(app) as client:
    data = b"abcdefghij"
    good = client.post("/upload", files={"file": ("a.txt", data, "text/plain")})
    assert good.status_code == 200
    assert good.json() == {"size": 10, "sha256": hashlib.sha256(data).hexdigest()}
    assert client.post("/upload", files={"file": ("b.txt", data + b"k")}).status_code == 413
    assert client.post("/upload", files={"file": ("empty.txt", b"")}).json()["size"] == 0
print("exact size boundary, excessive byte and content digest checked")

Размер — одна из проверок файла

Content-Type тоже задаётся клиентом и не доказывает формат. Для изображения используйте декодер с ограничениями, для архива — отдельные пределы распаковки, для таблицы — поддерживаемый парсер. Нулевой файл в примере разрешён; если операция требует содержимое, добавьте явный отказ и тест. Это решение должно следовать назначению загрузки.

finally закрывает временный объект и при успехе, и при 413. Если данные пишутся во временный файл назначения, удаляйте незавершённый результат при отказе и делайте финальное перемещение после полной проверки. Не оставляйте частично загруженный документ под именем готового.

Как проверить настоящий путь загрузки

Локальный TestClient подтверждает разбор multipart, вызов обработчика и лимит его обработки. Он не проверяет ограничение вашего reverse proxy и поведение на медленной сети. После настройки внешнего лимита выполните отдельный запрос на тестовый публичный адрес с файлом выше границы и убедитесь, какой слой возвращает 413.

Большим загрузкам часто нужен отдельный путь: например, прямая отправка в объектное хранилище по ограниченной ссылке с последующей проверкой метаданных и содержимого. Начните с разумного предела простого endpoint, а переход к другой архитектуре делайте по реальному размеру и сценарию файлов.

Следующая практика

Сначала закрепите POST и HTTP-валидацию в открытом уроке первой модели запроса. Загрузка multipart использует другой формат тела, но тот же порядок работы: договор входа, обработка допустимых данных и проверка отказа. Откройте базовую практику POST.

Продолжить устройство API можно в бесплатном курсе FastAPI: маршруты, модели, база и тесты соединяются в один проект. Для собственного файлового endpoint добавьте к этой основе проверку содержимого и границ размера. Откройте программу курса.

Продолжить на практике