Загрузка больших файлов в PHP: типичные ошибки и как их избежать
Форма отдаёт «413 Request Entity Too Large» или белый экран при заливке файла на 200 МБ. Разбираем, какие лимиты PHP и веб-сервера режут загрузку и как их согласовать между собой.

Пользователь пытается загрузить видео на 300 МБ, а форма отдаёт 413 Request Entity Too Large ещё до того, как запрос дойдёт до PHP. Либо загрузка доходит до PHP, но $_FILES оказывается пустым, а в логе — POST Content-Length exceeds the limit. Проблема почти всегда не в коде обработчика, а в цепочке лимитов: их несколько, они живут в разных местах, и достаточно одного заниженного, чтобы всё сломалось.
Разбираем цепочку целиком: где стоят ограничения, в каком порядке они срабатывают, какое сообщение об ошибке соответствует какому лимиту. Тема про потоковую загрузку через JavaScript-чанки — отдельная, здесь речь про обычную multipart-форму и стандартный move_uploaded_file. Проверено на PHP 8.2/8.3 с PHP-FPM за Nginx.
Где стоят лимиты и в каком порядке они срабатывают
Запрос с файлом проходит несколько слоёв, и каждый может оборвать загрузку. Порядок важен: если файл режет Nginx, до PHP запрос вообще не дойдёт, и настройки в php.ini ничего не изменят.
- Веб-сервер (Nginx или Apache) — проверяет размер тела запроса первым. У Nginx это
client_max_body_size, значение по умолчанию — 1 МБ. Превышение отдаёт413ещё до передачи в PHP. - PHP: размер тела POST — директива
post_max_size. Ограничивает весь POST-запрос целиком, включая все поля формы и файлы. - PHP: размер одного файла —
upload_max_filesize. Применяется к каждому файлу по отдельности. - PHP: время и память —
max_execution_time,max_input_time,memory_limit. На больших файлах в игру вступают именно они.
Ключевое правило соотношения: post_max_size должен быть больше upload_max_filesize, а client_max_body_size в Nginx — не меньше post_max_size. Если хотя бы одно неравенство нарушено, самый маленький лимит и станет реальным потолком.
Пустой$_FILESбез единой ошибки в браузере — почти всегда признак того, что POST-запрос превысилpost_max_size. PHP в этом случае обнуляет$_POSTи$_FILES, а сам факт превышения виден в$_SERVER['CONTENT_LENGTH'].
Ошибка 1: настроили только php.ini и забыли про веб-сервер
Самый частый случай. Вы выставили upload_max_filesize = 500M, перезапустили PHP-FPM, но Nginx по-прежнему отдаёт 413 на файле в 50 МБ. Потому что client_max_body_size в конфиге Nginx остался равным дефолтному 1 МБ.
Правьте в конфиге сайта Nginx — внутри блока server или location:
# /etc/nginx/sites-available/ваш-сайт.conf
server {
# разрешаем тело запроса до 512 МБ
client_max_body_size 512M;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
# увеличиваем таймаут ответа от PHP-FPM,
# иначе долгая загрузка оборвётся по 504
fastcgi_read_timeout 300;
}
}
После правки проверьте синтаксис и перезагрузите: nginx -t && systemctl reload nginx. Значение 0 у client_max_body_size отключает проверку размера совсем — так делать не стоит, это открывает сервер для запросов произвольного объёма.
Для Apache с mod_php отдельного аналога нет — там размер тела ограничивает сам PHP. Но если Apache проксирует на PHP-FPM или стоит за балансировщиком, лимиты будут и там.
Ошибка 2: post_max_size меньше или равен upload_max_filesize
Директива post_max_size ограничивает весь запрос, а не только файл. Если у вас upload_max_filesize = 100M и post_max_size = 100M, то файл на 100 МБ уже не пройдёт: к его размеру добавляются границы multipart, служебные заголовки и остальные поля формы. Итог — обрезанный $_FILES.
Правьте в php.ini активного SAPI. Узнать, какой файл читается, поможет вызов php --ini из CLI или phpinfo() из-под веб-сервера — путь у CLI и FPM разный.
; /etc/php/8.3/fpm/php.ini
upload_max_filesize = 200M
post_max_size = 210M ; заведомо больше upload_max_filesize
max_file_uploads = 20 ; максимум файлов в одном запросе
После правки php.ini для FPM нужен именно перезапуск, а не reload: systemctl restart php8.3-fpm. Проверить, что значения применились, можно так:
php-fpm8.3 -i | grep -E 'upload_max_filesize|post_max_size'
Учтите: если проект использует .htaccess или .user.ini, значения оттуда могут перекрыть php.ini. Для FPM файл .user.ini кешируется на время user_ini.cache_ttl (по умолчанию 300 секунд) — изменения применятся не мгновенно.
Ошибка 3: код не проверяет $_FILES['file']['error']
Многие обработчики сразу зовут move_uploaded_file, не глядя в код ошибки. А PHP при превышении лимитов не выбрасывает исключение — он кладёт числовой код в $_FILES['file']['error']. Игнорирование этого поля превращает провалившуюся загрузку в тихий баг.
<?php
// обработчик загрузки, PHP 8.x
if (!isset($_FILES['upload'])) {
// сюда попадаем, если POST превысил post_max_size:
// $_FILES обнулён, но CONTENT_LENGTH ненулевой
if (($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
http_response_code(413);
exit('Файл превысил допустимый размер запроса');
}
exit('Файл не передан');
}
$file = $_FILES['upload'];
// разбираем код ошибки до любых действий с файлом
switch ($file['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_INI_SIZE:
exit('Файл больше upload_max_filesize');
case UPLOAD_ERR_FORM_SIZE:
exit('Файл больше MAX_FILE_SIZE из формы');
case UPLOAD_ERR_PARTIAL:
exit('Файл загружен частично — оборвалось соединение');
case UPLOAD_ERR_NO_FILE:
exit('Файл не выбран');
case UPLOAD_ERR_NO_TMP_DIR:
exit('Нет временной директории для загрузки');
case UPLOAD_ERR_CANT_WRITE:
exit('Не удалось записать файл на диск');
default:
exit('Неизвестная ошибка загрузки: ' . $file['error']);
}
// перемещаем именно через move_uploaded_file,
// а не rename — функция проверяет, что файл пришёл через HTTP POST
$target = '/var/www/uploads/' . bin2hex(random_bytes(8));
if (!move_uploaded_file($file['tmp_name'], $target)) {
exit('Не удалось сохранить файл');
}
echo 'Файл сохранён';
Константы UPLOAD_ERR_* — часть ядра PHP, они существуют во всех актуальных версиях. Не сравнивайте error с числами напрямую (== 1): читаемость страдает, а смысл констант понятнее.
Ошибка 4: упираемся во время и память при обработке
Сама загрузка файла на диск памяти почти не ест — PHP пишет во временный файл потоково. Но дальнейшая обработка (чтение целиком в строку через file_get_contents, ресайз изображения, распаковка архива) может выйти за memory_limit. Файл на 200 МБ, прочитанный в переменную, — это минимум 200 МБ в памяти процесса.
Что происходит на практике:
- Долгая загрузка обрывается по таймауту. За приём тела запроса отвечает
max_input_time— по умолчанию оно равноmax_execution_time. На медленном канале большой файл может не успеть залиться. Если файл режется на этапе приёма, увеличивайтеmax_input_time, а не толькоmax_execution_time. - Nginx отдаёт 504. Если обработка после загрузки занимает больше
fastcgi_read_timeout, Nginx закроет соединение, даже если PHP ещё работает. Увеличивайте таймаут в конфиге Nginx (см. ошибку 1). - PHP падает с Allowed memory size exhausted. Здесь спасает не рост
memory_limitдо бесконечности, а потоковая обработка черезfopen/freadвместо чтения файла целиком.
Для операций с файлом читайте его порциями:
<?php
// потоковое чтение вместо file_get_contents — память не растёт с размером файла
$in = fopen($file['tmp_name'], 'rb');
$out = fopen($target, 'wb');
while (!feof($in)) {
fwrite($out, fread($in, 8192)); // читаем блоками по 8 КБ
}
fclose($in);
fclose($out);
Сводная таблица лимитов
| Директива / настройка | Где живёт | Что ограничивает | Сообщение при превышении |
|---|---|---|---|
client_max_body_size | Nginx, блок server/location | Размер всего тела запроса | 413 Request Entity Too Large |
post_max_size | php.ini (FPM) | Весь POST целиком | Пустой $_FILES, warning в логе |
upload_max_filesize | php.ini (FPM) | Один загружаемый файл | UPLOAD_ERR_INI_SIZE (код 1) |
max_input_time | php.ini (FPM) | Время приёма запроса | Обрыв на этапе загрузки |
fastcgi_read_timeout | Nginx, location php | Ожидание ответа от FPM | 504 Gateway Timeout |
memory_limit | php.ini (FPM) | Память процесса при обработке | Allowed memory size exhausted |
Что делать дальше
После согласования лимитов настройте проверку реального размера в самом коде — не полагайтесь только на php.ini. Проверяйте $_FILES['upload']['size'] и, если работаете с изображениями или конкретными форматами, MIME-тип через finfo, а не через присланный браузером type (его легко подделать).
В проде смотрите на два показателя: заполнение диска во временной директории загрузок (её путь задаёт upload_tmp_dir, по умолчанию системный /tmp) и количество 413/504 в логах Nginx. Первое подскажет, что незавершённые загрузки не подчищаются, второе — что цепочка лимитов где-то всё ещё рассогласована.
Если файлы регулярно превышают сотни мегабайт, обычная форма — не лучший инструмент: стоит смотреть в сторону загрузки чанками через JavaScript с докачкой. Но это уже другая тема и другая архитектура обработчика.
Помощник по курсовой и диплому: Большая работа поэтапно: от плана до защиты
Спросить за 25 ₽Источники: PHP Manual: Handling file uploads, Nginx: client_max_body_size directive
Частые вопросы
У меня Apache с mod_php, а не Nginx. Что менять?
Изменил php.ini, но лимиты остались старыми. Почему?
$_FILES приходит пустым, но браузер не показывает ошибку. В чём дело?
А если загрузка обрывается только на медленном интернете?
Насколько безопасно ставить client_max_body_size 0 или очень большое значение?
Работает ли всё это так же на PHP 7.4?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.