Почему nginx отдаёт 413, хотя лимит загрузки вы уже подняли
Товаровед грузит прайс поставщика на 43 мегабайта в админку интернет-магазина, а сайт через полсекунды отвечает «413 Request Entity Too Large». Админ идёт в nginx.conf, ставит client_max_body_size 256m, делает reload — и ничего не меняется. Дальше обычно начинается перебор наугад: правят PHP, приложение, снова nginx, ставят 0 «чтобы наверняка». Ниже — почему правка не долетает до нужного адреса, в какой именно момент nginx проверяет размер тела, как за десять минут найти реальный лимит из четырёх возможных и какой конфиг я ставлю на боевые стенды.
413 отдаёт nginx, а не ваше приложение
Первое, что я делаю по такой заявке, — не открываю конфиг. Открываю error.log фронтового nginx. Если размер тела зарубил именно он, там будет вполне однозначная строка, и половина диагноза готова ещё до того, как вы что-то тронули.
tail -n 500 /var/log/nginx/error.log | grep -i "too large"Типовая запись выглядит так:
2026/09/05 11:42:07 [error] 2411#2411: *83917 client intended to send
too large body: 45088768 bytes, client: 10.20.0.51,
server: shop.example.local, request: "POST /import/upload HTTP/1.1",
host: "shop.example.local"Здесь есть всё: сколько байт клиент собирался прислать, на какой server-блок он попал и каким методом. Обратите внимание на уровень — это [error], а не [warn], его не потеряешь в шуме.
Если такой строки нет вообще — значит 413 приехал не отсюда. Его вернул бэкенд, внутренний прокси, WAF или CDN провайдера. Отличать проще всего по заголовкам и телу ответа: собственная страница nginx компактная, с подписью версии внизу. Проверяю так:
curl -sS -o /dev/null -D - -X POST \
-H "Content-Type: application/octet-stream" \
--data-binary @/tmp/test-40m.bin \
https://shop.example.com/import/uploadСмотрю заголовок Server: и наличие Content-Type: text/html с нгинксовой заглушкой. Если в ответе JSON вашего приложения — искать надо в приложении. Учтите, что curl для тел больше мегабайта сам добавляет Expect: 100-continue, поэтому тестовый файл на 40 МБ по сети целиком не поедет, если лимит сработает сразу.
И ещё одна деталь, которая экономит нервы при разговоре с пользователем. В официальной документации прямым текстом сказано: браузеры не умеют корректно показывать эту ошибку. Поэтому сотрудник видит не «413», а «соединение сброшено», белую страницу или молчаливо зависший прогресс-бар. Описанию симптома от пользователя тут верить нельзя вообще — смотрите на сеть в DevTools или на логи.
- Зафиксируйте точный URL, на который уходит POST, — не адрес страницы, а адрес формы загрузки.
- Зафиксируйте реальный размер файла в байтах и размер всего тела запроса (multipart добавляет границы и заголовки полей).
- Проверьте, за сколько прилетает 413: мгновенно или после того, как ушла часть файла, — это разные механизмы.
- Убедитесь, что смотрите лог того самого nginx: во фронт-бэк-схеме их два, и логи у них разные.
Почему правка в nginx.conf не долетает до нужного адреса
Директива client_max_body_size допустима в трёх контекстах: http, server и location. Значение по умолчанию — 1m. Наследование работает сверху вниз, но с оговоркой, которую все знают и все забывают: нижний уровень наследует значение только тогда, когда у него нет собственного. Один явный client_max_body_size 1m; внутри location ~ \.php$ полностью перебивает ваши гордые 256 мегабайт, прописанные в http.
И такие строчки живут в конфигах не потому, что кто-то злой. Их приносят шаблоны панелей управления (в Hestia, например, конфиг домена целиком генерируется из шаблона), готовые сборки окружений под CMS, подключаемые сниппеты вида include snippets/php-fpm.conf;, а также сами разработчики — год назад, осознанно, чтобы кто-нибудь не залил гигабайт в публичный API. Про эту строчку никто не помнит, потому что она лежит на три include глубже, чем вы смотрели.
Единственный способ узнать, что nginx на самом деле собрал в память, — попросить его самого об этом рассказать. Ключ -T печатает полный итоговый конфиг со всеми раскрытыми include:
nginx -T 2>/dev/null | grep -n "client_max_body_size"Получив номера строк, смотрю окружение каждого вхождения — в каком именно server и location оно лежит:
nginx -T 2>/dev/null | sed -n '1,4000p' | grep -n -e "server_name" -e "location " -e "client_max_body_size"В восьми случаях из десяти виновник находится на этом шаге за одну минуту.
Отдельная категория промахов — правка сделана верно, но не применена. Классика: отредактировали файл, но забыли nginx -t && systemctl reload nginx. Или сделали reload, но systemd-юнит запускает nginx с ключом -c на другой конфиг: ручной nginx -t без -c проверяет файл по умолчанию (его путь виден в выводе и в nginx -V, параметр --conf-path), а не тот, что читает работающий процесс. Или правили на фронте, а режет внутренний nginx приложения. Или в контейнере правили смонтированный файл, а образ подкладывает свой при старте.
Правку применяю всегда одной связкой — сначала проверка синтаксиса, потом мягкое перечитывание:
nginx -t && systemctl reload nginxЕсли nginx -t ругается, reload не выполнится, и работающий процесс продолжит обслуживать запросы со старой, заведомо рабочей конфигурацией. Это важнее, чем кажется: restart с синтаксической ошибкой в конфиге оставит магазин вообще без веб-сервера.
- `nginx -T` — итоговый конфиг после всех include; всё остальное гадание.
- `nginx -t` — проверка синтаксиса файла по умолчанию; для нестандартного пути нужен `nginx -t -c <файл>`.
- `systemctl show nginx -p ExecStart` — проверить, нет ли `-c` на нестандартный файл.
- `grep -R client_max_body_size /etc/nginx/` — быстрый, но неполный вариант: не видит include из других каталогов.
- Reload, а не restart: перечитывание конфига не рвёт активные соединения.
Момент проверки: фаза выбора location
Тут кроется самое неочевидное. Maxim Dounin в рассылке nginx сформулировал это так: лимит проверяется только когда nginx выбирает location, а если Content-Length заранее неизвестен — то ещё и по мере чтения тела. Это не байка из блога, это подтверждается исходником: проверка clcf->client_max_body_size < r->headers_in.content_length_n живёт в функции ngx_http_core_find_config_phase, то есть ровно в фазе поиска конфигурации location.
Практическое следствие первое и главное: значение берётся из финального location, а не из того, на который смотрит браузер. Ваш location /import/ с честными 256m может вообще не участвовать в решении, если внутри стоит try_files $uri /index.php?$args; — после внутреннего редиректа запрос уезжает в location ~ \.php$, и лимит будет оттуда. То же самое с error_page, с именованными location типа @backend, с rewrite ... last. Каждый внутренний редирект — это новый выбор location и новый лимит.
Следствие второе: время появления 413 — диагностический признак, и он бесплатный. Если ошибка прилетает мгновенно, прогресс-бар не сдвинулся ни на процент, а в логе сообщение вида «too large body: 45088768 bytes» — значит Content-Length был известен, и nginx отбил запрос до чтения тела. Если же 413 приходит после того, как ушло несколько мегабайт, а в логе строка другая — «client intended to send too large chunked body: 8388608+65536 bytes» — значит тело шло chunked, длина заранее неизвестна, и проверка идёт по ходу приёма. Это разные ветки кода и иногда разные причины.
Следствие третье, приятное: nginx умеет отбивать 413 вообще не приняв ни байта тела. Когда клиент шлёт Expect: 100-continue (curl делает это автоматически, если тело известно или предполагается больше одного мегабайта), сервер отвечает 413 вместо 100 Continue. Поэтому проверять лимит с командной строки быстро и дёшево — файл на 400 мегабайт по сети не поедет — при условии, что клиент действительно дожидается ответа на Expect, вы получите отказ за миллисекунды.
- 413 мгновенно + «too large body: N bytes» → Content-Length известен, отбой на фазе выбора location.
- 413 в середине заливки + «too large chunked body: N+M bytes» → тело chunked, проверка по мере чтения.
- 413 без единой строки в error.log фронта → режет кто-то другой ниже по цепочке.
- Соединение просто рвётся без ответа → скорее всего таймаут (`client_body_timeout`, по умолчанию 60s) или промежуточный балансировщик, а не лимит размера.
Разбор: «РыболовГрад», импорт прайсов на 43 МБ и четыре лимита подряд
Рыболовный магазин «РыболовГрад», 48 рабочих мест: розничный зал, склад и интернет-магазин с обменом остатков с 1С. Схема для такого размера типовая: фронтовый nginx на Debian 12 с TLS и статикой, за ним по проксированию — второй nginx и PHP-FPM 8.3 на сервере приложения. Ветка nginx на обеих машинах — stable 1.30.x (актуальная mainline на момент разбора — 1.31.5). Заявка звучала буднично: «не грузится прайс поставщика снастей, файл 43 МБ, пишет 413». Плюс не заливались архивы с фотографиями товаров по 30–60 МБ.
Штатный админ магазина к моему приходу уже поставил в /etc/nginx/nginx.conf строку client_max_body_size 256m; в блоке http, сделал reload, потом на всякий случай ещё и restart. Не помогло ни разу. Через nginx -T | grep -n client_max_body_size нашлось четыре вхождения:
38: http { client_max_body_size 256m; # правка админа
214: server portal -> директивы нет (наследует 256m)
271: location ~ \.php$ { client_max_body_size 8m; # шаблон панели
305: location /api/ { client_max_body_size 1m; # поставил подрядчик год назадФорма загрузки уходила POST на /import/upload. В конфиге для этого пути отдельного location не было вообще: работал общий location / с try_files $uri $uri/ /index.php?$args;. То есть после внутреннего редиректа запрос попадал в location ~ \.php$ и получал ровно 8 мегабайт. Правка в http была технически корректной и абсолютно бесполезной.
Убрал 8m из php-локейшна (там он не нужен), добавил отдельный location = /import/upload с лимитом 256m. Перечитал конфиг. И получил… снова 413. Но уже другой: в error.log фронта чисто, а на внутреннем nginx приложения нашлась та же самая строка про too large body. Там конфиг никто никогда не трогал, и работал дефолт — 1 мегабайт. Урок ровно тот, о котором я писал выше: во фронт-бэк-схеме лимит нужно согласовать на каждом слое, а не на том, до которого вы первым добрались.
Поднял лимит на внутреннем — файл ушёл, приложение вернуло HTTP 200… с пустым результатом импорта. Это уже был PHP: post_max_size = 8M и upload_max_filesize = 2M из дистрибутивного php.ini. PHP в такой ситуации не отдаёт 413: при превышении post_max_size он оставляет $_POST и $_FILES пустыми и пишет в лог лишь предупреждение. Код UPLOAD_ERR_INI_SIZE появляется в другом случае — когда тело влезло в post_max_size, а отдельный файл превысил upload_max_filesize. Тихая потеря данных хуже честной ошибки: товаровед видит «импорт завершён», а в каталоге ноль обновлённых позиций. Поставил post_max_size = 300M, upload_max_filesize = 256M, max_file_uploads = 50, перечитал PHP-FPM (systemctl reload php8.3-fpm). memory_limit там стоял 512M, то есть больше post_max_size, как и советует документация PHP.
И финальный аккорд: архивы фотографий на 60 МБ стали отваливаться на 504 ровно через минуту. Это fastcgi_read_timeout со значением по умолчанию 60 секунд — разбор большого multipart и запись во внешнее хранилище в него не укладывались. Поднял до 300s точечно, на том же location, вместе с proxy_read_timeout и proxy_send_timeout на фронте. Итог: 43 МБ проходят за 6 секунд, пиковая занятость каталога временных тел — около 180 МБ при трёх параллельных загрузках (больше одновременно в магазине никто не грузит). До правок за два дня из 9 попыток импорта не прошло ни одной. Суммарно диагностика заняла 25 минут, правки — ещё десять; почти всё время съел поиск четвёртого лимита, потому что три предыдущих создавали иллюзию, что вот сейчас-то точно всё.
- Лимит №1 — `location ~ \.php$` на фронте: 8m вместо унаследованных 256m.
- Лимит №2 — внутренний nginx приложения: дефолтный 1m, конфиг не трогали никогда.
- Лимит №3 — PHP `post_max_size` 8M / `upload_max_filesize` 2M: 200 OK, пустые `$_POST` и `$_FILES`.
- Лимит №4 — `fastcgi_read_timeout` 60s: не размер, а время; вылез только на самых больших пакетах.
Как я настраиваю лимиты на боевых стендах
Моя позиция простая и она не всем нравится: глобально в http я держу небольшой разумный потолок, а большие значения выдаю точечно тем адресам, которым они действительно нужны. Обычно это 16m в http и отдельные 128–256m на конкретных endpoint импорта и загрузки. Так вы не открываете весь сайт для заливки чего угодно и одновременно не воюете с наследованием.
Про client_max_body_size 0 скажу прямо. Да, ноль полностью отключает проверку размера тела — это официально задокументированное поведение. Нет, я так не делаю на боевых серверах и вам не советую. Отключённая проверка означает, что любой скрипт, ошибка в мобильном клиенте или просто скучающий человек забьют раздел под client_body_temp_path до нуля свободного места, после чего у вас ляжет не форма импорта, а весь сервер. Ноль имеет право на жизнь ровно в одном сценарии: временно, на закрытом контуре, чтобы доказать, что дело именно в лимите.
http {
client_max_body_size 16m;
client_body_buffer_size 128k;
client_body_timeout 120s;
client_body_temp_path /var/lib/nginx/body 1 2;
server {
listen 443 ssl;
server_name shop.example.com;
location = /import/upload {
client_max_body_size 256m;
client_body_timeout 300s;
proxy_request_buffering on;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_pass http://app_backend;
}
location ~ \.php$ {
client_max_body_size 64m;
include fastcgi_params;
fastcgi_read_timeout 300s;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
}Пара пояснений к конфигу. client_body_buffer_size по умолчанию равен двум страницам памяти — 8k на x86 и x86-64, обычно 16k на прочих 64-битных платформах; всё, что не влезло, уходит во временный файл. Поднимать его выше 128–256k я смысла не вижу: память умножается на число одновременных загрузок, а выигрыш по диску копеечный. proxy_request_buffering по умолчанию включён — nginx сначала целиком примет тело, потом пойдёт к бэкенду. Выключать его (off) стоит, когда вы гоните действительно большие потоки и не хотите класть их на диск фронта, но помните цену: если тело уже начало уходить в апстрим, запрос нельзя переотправить на следующий сервер из группы.
Отдельно — про запас по размеру. Лимит считается по всему телу HTTP-запроса, а не по файлу. Multipart-форма добавляет границы и заголовки полей, а если приложение зачем-то шлёт файл в base64 внутри JSON, тело раздувается примерно на треть. Поэтому я всегда ставлю лимит с запасом хотя бы в полтора раза от максимального ожидаемого файла и обязательно проверяю, что дальше по цепочке (PHP post_max_size, лимиты бэкенда) стоят значения не меньше.
Про client_body_temp_path отдельно. По умолчанию это каталог client_body_temp, путь к которому задаётся при сборке: в пакетах Debian и Ubuntu это обычно /var/lib/nginx/body, в пакетах с nginx.org — /var/cache/nginx/client_temp. Точное значение показывает nginx -V (параметр --http-client-body-temp-path). Каталог должен быть доступен на запись пользователю рабочих процессов, а раздел под ним — иметь запас хотя бы на несколько одновременных максимальных загрузок. Если места не хватит, клиент получит уже не 413, а 500, и в error.log появится ошибка записи временного файла.
nginx -V 2>&1 | tr ' ' '\n' | grep temp-path
df -h /var/lib/nginx- В `http` — скромный потолок (16m); большие значения только на конкретных location.
- Лимит на каждом следующем слое цепочки не меньше, чем на предыдущем.
- Запас минимум ×1,5 от размера файла: multipart и base64 раздувают тело.
- Мониторинг свободного места на разделе с `client_body_temp_path` — обязателен.
- `client_max_body_size 0` — только для короткой проверки гипотезы, не для продакшена.
Соседние грабли: кто ещё режет тело запроса
nginx редко бывает единственным ограничителем. Ниже — список слоёв, которые я проверяю по порядку, когда правка client_max_body_size не дала эффекта. Начинаю всегда сверху, от клиента, потому что чем выше по цепочке стоит ограничитель, тем раньше он отбивает и тем меньше следов оставляет в ваших логах.
PHP заслуживает отдельного абзаца, потому что ведёт себя коварно. Директивы post_max_size и upload_max_filesize в дистрибутивных php.ini живут со скромными значениями (по документации php.net значения по умолчанию — 8M и 2M соответственно), и при превышении post_max_size интерпретатор не возвращает 413 — он отдаёт нормальный HTTP 200 с пустыми суперглобалами. Приложение честно рапортует об успехе, а данных нет. Если ваш импорт «проходит, но ничего не импортирует» — начинайте с php -i | grep -e post_max_size -e upload_max_filesize для того самого SAPI, который используется, а не для CLI: значения там нередко разные.
WAF-слой обычно самый неожиданный. У ModSecurity есть отдельные лимиты на тело запроса, и они по умолчанию сильно меньше того, что вы ожидаете от файловой загрузки. Проверять надо не по памяти и не по статьям, а в конфигурации вашей конкретной версии и вашего набора правил — цифры в разных сборках и разных версиях CRS отличаются. То же самое касается CDN и облачных прокси: у каждого провайдера свой потолок на размер запроса, он зависит от тарифа и меняется от года к году, так что единственный корректный источник — актуальная документация вашего тарифа, а не чей-то пост трёхлетней давности.
И ещё одна честная оговорка. Часть якобы «лимитных» проблем на самом деле таймауты. client_body_timeout по умолчанию 60 секунд и считается не на всю передачу, а между двумя последовательными операциями чтения — то есть медленный клиент с обрывами получит 408, а не 413. Если пользователь заливает 200 МБ через ADSL или мобильный интернет, у вас будет ровно эта картина, и повышение лимита размера не поможет вообще.
Для PHP-FPM важно ещё и то, где именно задаётся значение. Обе директивы имеют режим изменения PHP_INI_PERDIR: ini_set() из кода их не поменяет, а задать можно в php.ini, в .user.ini каталога сайта или в конфиге пула. Я предпочитаю пул — так значения видны администратору и не перетираются разработчиком:
; /etc/php/8.3/fpm/pool.d/shop.conf
php_admin_value[post_max_size] = 300M
php_admin_value[upload_max_filesize] = 256M
php_admin_value[max_file_uploads] = 50php-fpm8.3 -t && systemctl reload php8.3-fpmЗначение, заданное через php_admin_value, нельзя переопределить ни в .user.ini, ни через ini_set(). Проверяю итог не через php -i, а через phpinfo() или отладочный скрипт, отработавший именно в этом пуле.
- CDN / облачный прокси провайдера — свой потолок на размер запроса, смотреть тариф.
- WAF (ModSecurity и подобные) — отдельные лимиты на тело и на тело без файлов.
- Балансировщик или второй nginx во внутреннем контуре — дефолт 1m, о нём забывают чаще всего.
- Apache перед приложением — `LimitRequestBody` в конфиге виртуального хоста.
- PHP — `post_max_size`, `upload_max_filesize`, `max_file_uploads`; молчаливое обнуление `$_FILES`.
- Java-контейнеры (Tomcat и родня) — `maxPostSize` и `maxSwallowSize` в коннекторе.
- IIS / ASP.NET — `maxAllowedContentLength` в requestFiltering и `maxRequestLength` в httpRuntime.
- Само приложение — валидация размера в коде, часто зашитая константой.
Где править лимит в Kubernetes, Hestia и Bitrix-окружении
В Kubernetes с ingress-nginx лезть в nginx.conf внутри пода бессмысленно: контроллер генерирует его сам и перезапишет правку при следующей синхронизации. Лимит на уровне конкретного Ingress задаётся аннотацией nginx.ingress.kubernetes.io/proxy-body-size, глобальный — ключом proxy-body-size в ConfigMap контроллера. Без них действует дефолт nginx — те же 1m.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "256m"
spec:
ingressClassName: nginx
rules:
- host: shop.example.com
http:
paths:
- path: /import
pathType: Prefix
backend:
service:
name: shop
port:
number: 80Проверяю, что значение доехало, прямо в сгенерированном конфиге: kubectl exec -n ingress-nginx <под-контроллера> -- nginx -T | grep client_max_body_size. Учтите, что сам проект ingress-nginx сообщество Kubernetes вывело из активной поддержки, поэтому на новых кластерах я смотрю в сторону Gateway API; на существующих аннотация продолжает работать.
В HestiaCP конфиг домена не пишется руками: он собирается из шаблонов в /usr/local/hestia/data/templates/web/nginx/ (nginx как прокси перед Apache) или /usr/local/hestia/data/templates/web/nginx/php-fpm/ (связка nginx + PHP-FPM), а пулы PHP — из /usr/local/hestia/data/templates/web/php-fpm/. Документация Hestia прямо предупреждает: при пересборке пользователя или домена, при обновлении панели конфиги домена перезаписываются из шаблонов. Поэтому правка в /home/<user>/conf/web/<домен>/ живёт до первого rebuild. Правильный путь — скопировать штатный шаблон под своим именем (.tpl и .stpl для HTTP и HTTPS), добавить в нужный location client_max_body_size, назначить шаблон домену в панели и пересобрать v-rebuild-user <user>.
С Bitrix логика та же, только слоёв обычно больше. Если сайт стоит на Hestia, лимиты nginx правятся через собственный шаблон домена, а post_max_size и upload_max_filesize — через собственный шаблон пула PHP-FPM с именем вида bitrix-PHP-8_3.tpl, иначе после ближайшей пересборки они вернутся к дефолтам. В «1С-Битрикс: Веб-окружении» nginx и PHP настраиваются собственными конфигами окружения, и перед правкой я сначала ищу там все вхождения через nginx -T и php-fpm -i/phpinfo(), а не редактирую первый попавшийся файл. Сам Bitrix дополнительно показывает в «Проверке системы» итоговые PHP-лимиты, и это удобный способ убедиться, что правка доехала до того пула, который обслуживает сайт.
- ingress-nginx: аннотация `nginx.ingress.kubernetes.io/proxy-body-size` на Ingress или ключ `proxy-body-size` в ConfigMap; nginx.conf пода не трогать.
- Hestia: копия штатного шаблона под своим именем, затем назначение домену и `v-rebuild-user`; прямые правки в conf/web перетираются.
- Hestia + PHP: свой шаблон пула в `templates/web/php-fpm/` с `php_admin_value` для `post_max_size` и `upload_max_filesize`.
- Bitrix: сверить итоговые PHP-лимиты в «Проверке системы» и в phpinfo() того пула, что обслуживает сайт.
- После любой правки: `nginx -t` и reload, для PHP-FPM — проверка конфига и reload пула.
Чек-лист: найти виновника за десять минут
Порядок отработан на десятках заявок и почти всегда даёт ответ до восьмой позиции. Смысл в том, чтобы каждый шаг сужал зону поиска, а не добавлял новую правку в конфиг. Правки — в конце, когда виновник известен.
Если после прохода по списку вы всё ещё не нашли, кто отбивает запрос, — включайте на время client_body_in_file_only clean; и error_log ... debug; на конкретном server-блоке. Уровень debug работает только если nginx собран с --with-debug — проверяется через nginx -V; в пакетах nginx.org для этого есть отдельный бинарник nginx-debug. Debug-лог у nginx подробный до неприличия, но именно в такой ситуации он окупается: там видно и выбранный location, и решение по размеру тела, и момент, когда запрос ушёл в апстрим.
И последнее. Проверять исправление надо тем же способом, каким пользователь ловил ошибку — из браузера, из своей сети, с реальным файлом. Curl с сервера на localhost проходит мимо CDN, мимо WAF, часто мимо TLS-терминации и половины цепочки. Я видел не одну «починку», которая существовала только в терминале администратора.
- 1. Найти строку «too large body» в error.log фронтового nginx. Есть — идём дальше по nginx; нет — искать ниже по цепочке.
- 2. Посмотреть, мгновенный это 413 или в середине заливки: обычное тело или chunked.
- 3. `nginx -T | grep -n client_max_body_size` — увидеть ВСЕ вхождения, включая include и сниппеты панели.
- 4. Определить, в какой location запрос попадает ПОСЛЕ rewrite/try_files, и смотреть лимит именно там.
- 5. Проверить `nginx -t` (с `-c`, если юнит systemd запускает nginx с нестандартным конфигом) и был ли reload после правки.
- 6. Повторить шаги 1 и 3 на внутреннем nginx / балансировщике, если схема двухслойная.
- 7. Проверить лимиты интерпретатора: `post_max_size`, `upload_max_filesize` в том пуле PHP-FPM, что обслуживает сайт; в Kubernetes и Hestia — там, откуда конфиг генерируется.
- 8. Проверить WAF и CDN — по документации своей версии и тарифа, а не по памяти.
- 9. Разделить размер и время: 413 — это размер, 408/504 и обрыв — это таймауты.
- 10. Проверить итог из браузера пользователя, с реальным файлом, а не curl'ом с localhost.
Частые вопросы
Я поднял client_max_body_size в http-блоке, сделал reload, а 413 остался. Почему?
Скорее всего, на уровне server или location стоит собственное значение — оно перебивает унаследованное. Выполните `nginx -T | grep -n client_max_body_size` и посмотрите все вхождения, включая подключаемые сниппеты панели управления. Второй частый вариант — запрос после try_files или rewrite уходит в другой location (например, в `location ~ .php$`), и лимит берётся оттуда.
Можно ли просто поставить client_max_body_size 0 и забыть?
Технически да, ноль отключает проверку размера тела полностью — это задокументированное поведение. Но на боевом сервере я так не делаю: без лимита любой скрипт или сбойный клиент забьёт раздел под временные тела запросов, и ляжет весь сервер, а не одна форма. Используйте ноль только временно, чтобы подтвердить, что проблема действительно в лимите.
413 приходит не сразу, а после того как ушла половина файла. Это нормально?
Да, и это полезный признак. Когда Content-Length заранее неизвестен (chunked-передача), nginx проверяет размер по мере чтения тела и отбивает запрос в момент превышения. В логе при этом будет другое сообщение — «client intended to send too large chunked body». Если же 413 прилетает мгновенно, значит длина была объявлена в заголовке и отбой произошёл ещё до приёма тела.
Файл загружается, сервер отвечает 200, но импорт пустой. Это тоже про лимиты?
Почти наверняка да, только уже не про nginx. Классика — PHP: при превышении post_max_size суперглобальные $_POST и $_FILES оказываются пустыми, а ответ приложения — обычный 200. Проверьте post_max_size и upload_max_filesize именно для того SAPI, который обслуживает запрос (пул PHP-FPM, а не CLI); задавайте их в php.ini, .user.ini или через php_admin_value в пуле — ini_set() для них не работает, и приведите их в соответствие с лимитом nginx.
У меня фронтовый nginx и второй nginx на сервере приложения. Где править?
На обоих, и значение на внутреннем не должно быть меньше, чем на фронте. Определить, кто именно отбил, просто: смотрите, в чьём error.log появилась строка «too large body». Если на фронте пусто, а на внутреннем есть — правьте внутренний. Про второй слой забывают чаще всего, потому что его конфиг обычно никто никогда не открывал.
В Kubernetes поправил nginx.conf в поде ingress-контроллера, а через время снова 413. Почему?
Контроллер ingress-nginx сам генерирует nginx.conf и перезаписывает ручные правки. Лимит задаётся аннотацией nginx.ingress.kubernetes.io/proxy-body-size на конкретном Ingress или ключом proxy-body-size в ConfigMap контроллера. Не забудьте, что за ingress может стоять ещё и nginx или PHP внутри самого приложения со своими лимитами.
На сервере с HestiaCP правка client_max_body_size пропала сама. Кто её откатил?
Сама панель. Конфиги доменов в Hestia генерируются из шаблонов и перезаписываются при пересборке пользователя или домена и при обновлениях. Скопируйте штатный шаблон nginx (и при необходимости шаблон пула PHP-FPM) под своим именем, внесите лимит туда, назначьте шаблон домену и выполните v-rebuild-user.
Насколько актуальны эти директивы в свежих версиях nginx?
Полностью актуальны. Поведение client_max_body_size не менялось годами и одинаково работает и в stable-ветке (на сентябрь 2026 это 1.30.x), и в mainline (1.31.x). Меняются вокруг них смежные вещи — HTTP/2 и HTTP/3, буферизация запроса к апстриму, — но сама механика проверки размера тела остаётся прежней.
Источники
- Документация nginx (рус.), модуль ngx_http_core_module — client_max_body_size: умолчание 1m, контексты http, server, location, при превышении 413, значение 0 отключает проверку, браузеры не умеют корректно показывать ошибку; client_body_buffer_size (8k|16k), client_body_temp_path, client_body_timeout (60s, ошибка 408), client_body_in_file_only. https://nginx.org/ru/docs/http/ngx_http_core_module.html#client_max_body_size
- Документация nginx (рус.), модуль ngx_http_proxy_module — proxy_request_buffering (по умолчанию on), proxy_read_timeout и proxy_send_timeout (60s). https://nginx.org/ru/docs/http/ngx_http_proxy_module.html#proxy_request_buffering
- Рассылка nginx, ответ Maxim Dounin, 14.12.2018 — Тема «Nginx not enforcing default client_max_body_size ?»: лимит применяется при выборе location, а при заранее неизвестном Content-Length — при чтении тела. https://mailman.nginx.org/pipermail/nginx/2018-December/057291.html
- Исходный код nginx, src/http/ngx_http_core_module.c и ngx_http_request_body.c — Проверка Content-Length против client_max_body_size в ngx_http_core_find_config_phase; сообщение для chunked-тела — в ngx_http_request_body.c. https://github.com/nginx/nginx/blob/master/src/http/ngx_http_core_module.c
- Руководство PHP (php.net), описание директив php.ini — post_max_size (8M), upload_max_filesize (2M), max_file_uploads (20), режим PHP_INI_PERDIR; при превышении post_max_size $_POST и $_FILES пусты; memory_limit должен быть больше post_max_size. https://www.php.net/manual/ru/ini.core.php#ini.post-max-size
- Руководство PHP (php.net), ошибки загрузки файлов — Коды UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE и др. https://www.php.net/manual/ru/features.file-upload.errors.php
- Документация ingress-nginx, Annotations — Custom max body size: аннотация nginx.ingress.kubernetes.io/proxy-body-size, глобально — ключ proxy-body-size в ConfigMap. https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#custom-max-body-size
- Документация HestiaCP, Web templates and caching — Каталоги шаблонов nginx, nginx/php-fpm, php-fpm; шаблоны копировать под своим именем; конфиги доменов перезаписываются при rebuild; применение через v-rebuild-user. https://hestiacp.com/docs/server-administration/web-templates.html
- Everything curl, Expect 100-continue — curl по умолчанию отправляет Expect: 100-continue, если тело POST/PUT известно или предполагается больше одного мегабайта. https://everything.curl.dev/http/post/expect100.html
- RFC 9110, HTTP Semantics — Раздел 15.5.14 — статус 413 «Content Too Large» (прежнее название «Request Entity Too Large»). https://www.rfc-editor.org/rfc/rfc9110.html#name-413-content-too-large
