Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Загрузка файлов в веб-приложении: стриминг и лимиты

Загрузка файлов в веб-приложении: стриминг, лимиты и 413

Загрузка файлов: стриминг против буферизации в памяти, лимиты размера, client_max_body_size, временные файлы и обработка 413 Payload Too Large.

Прямой ответ

Загрузка файла проходит как запрос multipart/form-data, в котором тело разбито на части. Главная инженерная задача — не прочитать файл целиком в память, а обработать его потоково, и согласовать лимиты на всех уровнях пути запроса.

Типовая ошибка выглядит так: файл 200 МБ загружается на сервер, который держит его в оперативной памяти, и при пяти одновременных загрузках контейнер исчерпывает лимит памяти и перезапускается. Причина не в объёме диска, а в способе чтения.

Правильный подход — стриминг:

# FastAPI / Starlette: файл приходит как SpooledTemporaryFile
from fastapi import FastAPI, UploadFile, HTTPException

app = FastAPI()
MAX_SIZE = 50 * 1024 * 1024          # 50 МБ
CHUNK = 1024 * 1024                   # 1 МБ

@app.post("/upload")
async def upload(file: UploadFile):
    total = 0
    with open(f"/data/inbox/{uuid4().hex}", "wb") as out:
        while chunk := await file.read(CHUNK):
            total += len(chunk)
            if total > MAX_SIZE:
                raise HTTPException(status_code=413, detail="Файл больше 50 МБ")
            out.write(chunk)
    return {"size": total}

Чтение порциями даёт три преимущества: память не зависит от размера файла, превышение лимита обнаруживается в процессе, а не после полной загрузки, и соединение можно прервать сразу.

Лимиты задаются на нескольких уровнях, и запрос проходит через все:

Уровень Параметр Поведение при превышении
nginx / ingress client_max_body_size 413 до передачи тела приложению
Обратный прокси client_body_buffer_size Буферизация во временный файл
Приложение Собственная проверка 413 с понятным сообщением
Хранилище Квота бакета Ошибка записи

По умолчанию client_max_body_size в nginx равен 1 МБ, поэтому загрузка файла большего размера отклоняется на уровне прокси, и приложение об этом не узнаёт. Настройка:

location /upload {
    client_max_body_size 50m;
    client_body_timeout 300s;
    proxy_request_buffering off;     # стриминг без буферизации
    proxy_read_timeout 300s;
}

Флаг proxy_request_buffering off принципиален: без него nginx сначала принимает всё тело на диск и только затем передаёт приложению, что удваивает задержку и расход места. Детали настройки прокси — в статье про nginx.

Практика: надёжный приём файлов

1. Никогда не доверяйте имени файла из запроса. Поле filename задаётся клиентом и может содержать ../ или управляющие символы. Имя генерируйте на сервере, а исходное сохраняйте как метаданные.

2. Проверяйте размер до записи, а не после. Заголовок Content-Length можно подделать или не отправить при chunked-передаче, поэтому реальный подсчёт байтов в цикле обязателен.

3. Различайте 413 и таймаут. 413 означает превышение размера; разрыв соединения во время передачи чаще связан с таймаутом прокси. Сообщения об ошибке должны отличаться — см. статью про обработку ошибок.

4. Учитывайте лимиты контейнера. Процесс, читающий файл в память, потребляет её сверх базового уровня; лимит памяти пода должен это учитывать, иначе загрузка приводит к OOMKill — см. статью про requests и limits.

5. Пишите во временный файл и переименовывайте. Запись сразу в целевой путь оставляет неполный файл при обрыве соединения. Переименование в пределах одной файловой системы атомарно.

6. Логируйте факт загрузки без содержимого. В логе должны быть размер, контрольная сумма и идентификатор, но не данные файла.

7. Ограничьте параллелизм. Десять одновременных загрузок по 50 МБ — это 500 МБ входящего трафика и соответствующая нагрузка на диск.

Рекомендации:

  1. Вынесите приём больших файлов из приложения. Presigned URL позволяет клиенту писать напрямую в объектное хранилище, минуя сервер приложения.
  2. Возвращайте идентификатор, а не путь. Клиенту нужен токен загруженного файла, а не расположение в файловой системе.
  3. Обрабатывайте повторную загрузку. Сеть рвётся, и клиент повторит запрос; идемпотентность по контрольной сумме предотвращает дубли.
  4. Не храните файлы на локальном диске пода. Он исчезает при пересоздании пода; нужно внешнее хранилище.
  5. Проверяйте тип содержимого по данным, а не по расширению. Расширение задаёт клиент и оно ничего не гарантирует.

Практический сценарий: intermittent 502 при загрузке отчётов

Контекст: пользователи загружали CSV-отчёты объёмом до 80 МБ. Примерно каждая пятая загрузка завершалась ошибкой 502, остальные проходили успешно.

  1. Диагностика. В логах приложения записей о неудачных загрузках не было — значит, запрос не доходил до обработчика. В логах nginx нашлась строка client intended to send too large body.
  2. Проверка конфигурации. client_max_body_size был установлен в 100m для основного location /, но путь /api/upload попадал в другой блок, где действовало значение по умолчанию 1m.
  3. Уточнение. Ошибка 502 вместо ожидаемой 413 возникала из-за того, что прокси закрывал соединение до отправки ответа, а клиент интерпретировал это как сбой шлюза.
  4. Действия. (а) Вынесли настройку размера на уровень server, чтобы она действовала для всех путей; (б) включили proxy_request_buffering off, чтобы тело не дублировалось на диске прокси; (в) увеличили client_body_timeout до 300 с для медленных каналов; (г) добавили в приложении подсчёт байтов с возвратом 413 и понятным сообщением; (д) добавили в мониторинг счётчик ответов 413 и 502 по пути загрузки.
  5. Результат. Ошибки 502 прекратились; превышение размера теперь возвращается как 413 с указанием лимита, а метрика позволяет увидеть всплеск неудачных загрузок.

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

FAQ

Почему nginx возвращает 413, хотя приложение разрешает больший размер? Прокси проверяет размер раньше приложения. По умолчанию лимит nginx составляет 1 МБ, поэтому его нужно поднимать явно и согласовывать с приложением.

Чем стриминг отличается от буферизации на диск? При буферизации прокси сначала сохраняет всё тело локально, затем передаёт его приложению. Стриминг передаёт данные по мере поступления, что снижает задержку и расход диска, но требует обработки ошибок в процессе.

Как ограничить размер, если клиент не отправляет Content-Length? Считать байты в цикле чтения и прерывать обработку при превышении. Заголовок может отсутствовать при chunked-передаче, поэтому полагаться на него нельзя.

Что делать с файлом, если обработка после загрузки завершилась ошибкой? Удалять временный файл в блоке очистки и вести учёт незавершённых загрузок для периодической очистки. Иначе хранилище постепенно заполняется осиротевшими файлами.

Источники: MDN (413), RFC 7578 (multipart/form-data), nginx docs — ссылки в шапке.