АйТи Фреш
Главная / Статьи / DevOps и автоматизация
DevOps и автоматизация

Почему nginx отдаёт 413, хотя лимит загрузки вы уже подняли

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~27 мин чтения
Почему nginx отдаёт 413, хотя лимит загрузки вы уже подняли
Иллюстрация к статье «Почему 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 или на логи.

Не правьте ни одной строчки конфига, пока не увидели в error.log строку «client intended to send too large body». Без неё вы чините не тот сервер.
Порядок действий: 413 отдаёт nginx, а не ваше приложение — схема
Порядок действий: 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` — единственный источник правды о конфигурации. Если директивы там нет или она не та, никакие рассуждения о наследовании уже не важны.
Почему nginx отдаёт 413, хотя лимит загрузки вы уже подняли — схема
Схема к статье. Открыть схему в полном размере

Момент проверки: фаза выбора 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, вы получите отказ за миллисекунды.

Правьте лимит в том location, куда запрос приходит ПОСЛЕ всех rewrite и try_files. Проверить это просто: временно добавьте в подозрительные location `add_header X-Debug-Loc php always;` (с разными метками) или пишите в access_log переменную `$uri` рядом с `$request_uri` — первая показывает URI после внутренних перенаправлений.

Разбор: «РыболовГрад», импорт прайсов на 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 минут, правки — ещё десять; почти всё время съел поиск четвёртого лимита, потому что три предыдущих создавали иллюзию, что вот сейчас-то точно всё.

Если 413 «почти исчез», но появилась новая ошибка — вы не сломали ничего, вы просто дошли до следующего слоя. Проходите цепочку до конца: браузер → CDN/WAF → фронт-nginx → внутренний прокси → интерпретатор → приложение.
Цифры и версии: Разбор: «РыболовГрад», импорт прайсов на 43 МБ и четыре лимита подряд — схема
Цифры и версии: Разбор: «РыболовГрад», импорт прайсов на 43 МБ и четыре лимита подряд. Открыть схему в полном размере

Как я настраиваю лимиты на боевых стендах

Моя позиция простая и она не всем нравится: глобально в 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
Хотите отдавать вменяемую ошибку вместо белой страницы — сделайте это явно: ```nginx error_page 413 = @too_large; location @too_large { default_type application/json; return 413 '{"error":"file_too_large","limit_mb":256}'; } ``` Браузер всё равно может показать своё, но ваш SPA или мобильный клиент получат внятный ответ, а не разорванное соединение.

Соседние грабли: кто ещё режет тело запроса

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] = 50
php-fpm8.3 -t && systemctl reload php8.3-fpm

Значение, заданное через php_admin_value, нельзя переопределить ни в .user.ini, ни через ini_set(). Проверяю итог не через php -i, а через phpinfo() или отладочный скрипт, отработавший именно в этом пуле.

Точные значения по умолчанию у WAF, CDN и контейнеров приложений плавают от версии к версии и от тарифа к тарифу. Не полагайтесь на память и на статьи в интернете — читайте документацию к своей версии. Это ровно тот случай, когда «я точно помню, там 128 килобайт» стоит двух часов.

Где править лимит в 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-лимиты, и это удобный способ убедиться, что правка доехала до того пула, который обслуживает сайт.

В панелях и оркестраторах правка «на живом файле» — самая дорогая ошибка: сегодня она работает, а через неделю обновление панели или пересоздание пода молча возвращает 1m, и заявка «опять 413» приходит снова. Лимит должен жить там, откуда конфиг генерируется.

Чек-лист: найти виновника за десять минут

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

Если после прохода по списку вы всё ещё не нашли, кто отбивает запрос, — включайте на время 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-терминации и половины цепочки. Я видел не одну «починку», которая существовала только в терминале администратора.

Практический вывод в одну строку: 413 после правки лимита почти всегда означает, что вы подняли не тот лимит и не на том слое. Ищите не «как поднять», а «кто именно отбил» — и правка займёт минуту.
Порядок действий: Чек-лист: найти виновника за десять минут — схема
Порядок действий: Чек-лист: найти виновника за десять минут. Открыть схему в полном размере

Частые вопросы

Я поднял 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, буферизация запроса к апстриму, — но сама механика проверки размера тела остаётся прежней.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи