1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

4xx: чей это на самом деле код

Коротко

Код 4xx говорит категорию, а причину - строка в error_log. И главное: часть этих кодов nginx выдаёт сам, не спросив приложение, поэтому в логах бэкенда такого запроса нет вовсе.

  • 403 бывает трёх природ, и у каждой своя строка: нет прав, каталог без index-файла, сработавший deny.
  • 413 отдаётся до бэкенда: client_max_body_size по умолчанию 1 МБ, в логе upstream=-, время 0,001 с.
  • 404 надо читать по пути в кавычках, а не по URI из запроса.
  • 499 - код самого nginx: клиент ушёл, не дождавшись.
  • Объяснения 400 и 499 идут на уровне info и ни при warn, ни при error не пишутся.

Если три природы 403 и происхождение 413 и 499 знакомы - листай до «Теперь сам».

Сначала ответь сам

Пользователь не может загрузить видео: маленькие картинки уходят, файл на 26 мегабайт обрывается ошибкой. Разработчик смотрит логи приложения и говорит: «У меня такого запроса нет. Это не мы».

Он прав. Строка в логе nginx:

↳  выводgrep upload /var/log/nginx/main.log
127.0.0.1 - - [10/Sep/2026:19:59:03 +0000] "POST /api/upload HTTP/1.1" 413 183 "-" "curl/8.22.0" host=localhost rt=0.000 upstream=- us=- urt=- uct=- uht=- rid=db9d77ba17a687ff9a098c91b8ce34a0

upstream=- означает, что запрос до бэкенда не дошёл ни на миллиметр. nginx прочитал заголовок Content-Length, сравнил с client_max_body_size и ответил за одну миллисекунду, тела даже не читая:

↳  выводtail -n 1 /var/log/nginx/error.log
2026/09/10 19:59:03 [error] 8#8: *12 client intended to send too large body: 26000000 bytes, client: 127.0.0.1, server: shop.local, request: "POST /api/upload HTTP/1.1", host: "localhost"

8#8 - номер процесса и потока, *12 - номер соединения: по нему в логе находятся все строки одного и того же клиента.

Разговор «это не мы» заканчивается на слове upstream=-.

На что это похоже

У здания есть турникет, а внутри - отдел, куда человек шёл. Часть посетителей разворачивают на турникете: нет пропуска, слишком большая сумка, странно заполненный бланк. В отделе про них не знают ничего - там не было ни звонка, ни записи.

Идти в отдел с вопросом «почему его не приняли» бессмысленно. Смотреть надо журнал охраны, и там записано, на чём именно развернули.

4xx делятся ровно так: часть кодов - решение турникета, часть - решение отдела.

Механизм: где именно запрос умирает

разбор запроса 400 - битая строка или заголовок приём тела 413 - тело больше лимита проверка доступа 403 - deny, нет прав, нет index поиск файла 404 - по пути ничего нет ожидание ответа 499 - клиент ушёл первым
До бэкенда доживает только то, что прошло все четыре верхние ступени; 499 может случиться и позже, уже во время ожидания.

Правило

Сначала посмотри, есть ли в строке upstream. Прочерк означает, что решение принял nginx, и разбираться надо в его конфиге. Адрес означает, что запрос дошёл, и код мог прийти от приложения.

Разбор: 403 в трёх видах

Три запроса на стенде, три одинаковых кода, три разные причины:

grep -v выбрасывает строки со словом: записи про закрытые keepalive-соединения здесь только шум.

↳  выводgrep -v keepalive /var/log/nginx/error.log
[error] open() "/www/locked/a.txt" failed (13: Permission denied)
[error] directory index of "/www/dir_no_index/" is forbidden
[error] access forbidden by rule
Строка Причина Чем чинить
failed (13: Permission denied) воркеру не хватает прав на файл или на каталог пути владелец и права, x на каждый каталог пути
directory index of "..." is forbidden запрос на каталог, index-файла нет index, try_files или autoindex on
access forbidden by rule сработал deny правила allow/deny в location, вроде deny all;

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

404 читается тем же приёмом - по пути:

↳  выводtail -n 1 /var/log/nginx/error.log
[error] open() "/www/net/takogo.css" failed (2: No such file or directory)

Читай путь в кавычках, а не URI из запроса: он показывает, во что URI превратился после root и alias. Если файлы лежат в другом каталоге, чинить надо конфиг, а не выкладку. Номера в скобках - коды системного вызова: 2 - нет файла, 13 - нет прав.

Разбор: где наивное правило подводит

Половины строк при обычном уровне нет. Объяснения 400 и 499 nginx пишет на уровне info, а в конфигах стоит уровень выше: в пакете Debian он не назван, то есть действует error, а инструкции обычно советуют warn. Проверено на стенде: при warn за секунду с кодом 400 в error_log не появляется ничего. Опустишь уровень - увидишь:

↳  выводgrep "too long" /var/log/nginx/error.log
[info] client sent too long header line: "X-Big: aaaa..." while reading client request headers

Это, кстати, самая частая причина 400 в живом проекте: у приложения разрослись куки. Лечится large_client_header_buffers 4 16k; (умолчание - 4 8k: четыре буфера по 8 КБ), но сначала стоит спросить, зачем приложению заголовки на десятки килобайт.

400 бывает и там, где всё «по стандарту». Запрос без заголовка Host по HTTP/1.1 отвергается сразу, а тот же запрос по HTTP/1.0 отрабатывает нормально - в старой версии протокола Host необязателен. Проверить можно так: curl -H 'Host:' localhost/ убирает заголовок, а -0 переключает curl на HTTP/1.0. Проверено: 400 против 200 на одном и том же конфиге. Так что «у меня скриптом работает, а у них нет» иногда объясняется одной строкой в самодельном клиенте.

499 - это не поломка nginx. Кода нет в стандарте, его придумал сам nginx, и означает он «клиент закрыл соединение раньше, чем пришёл ответ». В строке получается любопытная картина:

↳  выводgrep 499 /var/log/nginx/main.log
"GET /api/slow HTTP/1.1" 499 0 rt=1.001 upstream=172.24.0.2:8000 us=- urt=1.001 uht=-

us=- и uht=- - бэкенд не успел отдать даже заголовки; urt=1.001 - столько nginx прождал до обрыва. Ровно секунда, потому что у клиента стоял таймаут в секунду, а бэкенд думал шесть.

За 499 обычно стоит одно из трёх: бэкенд отвечает дольше, чем ждёт клиент; таймаут стоит на CDN или балансировщике перед nginx; кто-то сканирует сайт и рвёт соединения, не читая ответы. Первое - самое частое, и лечится оно скоростью бэкенда, а не настройками nginx.

Что ломается без этого

Команда неделю ищет ошибку в приложении, которого запрос не касался. Классика - 413: пользователи жалуются, разработчик разводит руками, потому что в его логах чисто, а нужное слово client_max_body_size не произносит никто.

Второе - 403 «вообще». Без разбора на три природы человек начинает наугад раздавать права chmod 777, хотя строка в логе говорила access forbidden by rule: права ни при чём, кто-то поставил запрет намеренно.

Зачем это в работе

Список того, что кто-то ищет, а сервер не отдаёт:

$  команда
awk '$9 == 404 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

Условие перед фигурными скобками отбирает строки, где девятое поле - код - равно 404, и печатает у них седьмое - адрес. head оставляет первые десять.

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

Лимит на тело поднимают точечно, а не на весь сервер:

/etc/nginx/conf.d/shop.local.confhttp server
location /api/upload {
    client_max_body_size 100m;
    proxy_pass http://127.0.0.1:8080;
}

Сто мегабайт на весь сайт означают, что любой адрес вправе занять столько памяти и диска на каждый запрос. В том location, где действительно принимают файлы, лимит поднимают; везде ещё оставляют маленьким.

Вежливый клиент умеет спросить разрешение до отправки: он шлёт заголовок Expect: 100-continue и ждёт. nginx отвечает сам, не спрашивая бэкенд, и ответ зависит от лимита. Два обмена на стенде с client_max_body_size 1m: сначала тело в 5 байт, потом в 26 МБ (-v печатает заголовки обмена, > - то, что ушло, < - то, что пришло):

$  команда
curl -sv -H 'Expect: 100-continue' --data-binary @tiny localhost/api/upload 2>&1 | grep -E '^< HTTP|^> Expect'
↳  выводcurl -sv -H 'Expect: 100-continue' --data-binary @tiny localhost/api/upload 2>&1 | grep -E '^< HTTP|^> Expect'
> Expect: 100-continue
< HTTP/1.1 100 Continue
< HTTP/1.1 404 Not Found
$  команда
curl -sv -H 'Expect: 100-continue' --data-binary @big localhost/api/upload 2>&1 | grep -E '^< HTTP|^> Expect'
↳  выводcurl -sv -H 'Expect: 100-continue' --data-binary @big localhost/api/upload 2>&1 | grep -E '^< HTTP|^> Expect'
> Expect: 100-continue
< HTTP/1.1 413 Request Entity Too Large

В первом обмене nginx разрешил отправку, и дальше ответил бэкенд - здесь 404, такого адреса у него нет. Во втором 26 мегабайт по сети даже не поехали: отказ пришёл до тела. Поэтому curl при загрузке крупных файлов шлёт этот заголовок сам, а вот самодельные клиенты обычно нет - и гоняют файл впустую, чтобы получить тот же 413 в конце.

Теперь сам

В access_log пачка строк: код 499, rt около 30 секунд, us=-, все с одного адреса, все на /api/report. error_log на уровне warn молчит. Что происходит и что делать?

Клиент ждёт тридцать секунд и уходит, не дождавшись, - раз за разом. Виноват не nginx: us=- значит, что бэкенд за тридцать секунд не отдал даже заголовков, то есть действительно не успевает. Чинить надо отчёт на бэкенде либо делать его асинхронным. Молчание error_log объясняется уровнем: строку про ушедшего клиента пишут на info. А один адрес на все строки - повод посмотреть, не повторяет ли клиент запрос автоматически после каждого таймаута.

Главное

Прочерк в upstream означает, что код придумал nginx, а не приложение: 413 (по умолчанию лимит 1 МБ) отдаётся до чтения тела, 403 и 404 - при поиске файла и проверке доступа. У 403 три природы, и различает их строка: (13: Permission denied), directory index ... is forbidden, access forbidden by rule. В 404 читай путь в кавычках - это то, во что URI превратился. 499 - код самого nginx о клиенте, который не дождался. Объяснения 400 и 499 пишутся на уровне info и при warn не появляются вовсе.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.