Обработка ошибок в Nginx: сравнение подходов
Nginx может вернуть клиенту статичную страницу, передать ошибку приложению или оставить ответ upstream без изменений. Сравниваем эти подходы и показываем, где каждый из них ломается под нагрузкой.

Nginx может обработать ошибку сам, передать запрос во внутренний location или отдать клиенту ответ приложения без изменений. Неправильный выбор приводит к разным проблемам: пользователь получает HTML вместо JSON, прокси скрывает полезный статус, а страница ошибки начинает сама генерировать новую ошибку. Ниже сравниваются три рабочих подхода для Nginx Open Source 1.25.x и новее: статический error_page, внутренний обработчик через named location и передача ошибок upstream.
Что именно считается ошибкой в Nginx
У прокси есть два источника HTTP-ошибок. Nginx может сам сформировать ответ, например при недоступности upstream и статусе 502, либо получить от приложения уже готовый ответ 404 или 500.
Это различие определяет поведение конфигурации. Директива error_page обрабатывает локальные ошибки Nginx. Чтобы перехватывать ответы приложения со статусом 4xx или 5xx, для прокси обычно нужен proxy_intercept_errors on;. Без него Nginx передаст такой ответ клиенту как есть.
Термин. Upstream — сервер или группа серверов, куда Nginx пересылает запрос через proxy_pass. Это может быть приложение на Node.js, PHP-FPM через FastCGI или отдельный HTTP-сервис.
Статическая страница хороша для аварии инфраструктуры, а приложение — для ошибки бизнес-логики. Если поменять их местами, вы потеряете либо контекст, либо устойчивость.
Три подхода к обработке ошибок
1. Статическая страница через error_page
Это самый простой и устойчивый вариант. Nginx получает ошибку и делает внутренний переход на заранее подготовленный HTML-файл.
http {
error_page 404 /errors/404.html;
error_page 500 502 503 504 /errors/50x.html;
server {
listen 80;
server_name example.com;
root /var/www/site;
location = /errors/404.html {
internal;
}
location = /errors/50x.html {
internal;
}
}
}
Флаг internal запрещает открыть служебный URL напрямую. Файл при этом должен физически находиться внутри значения root, например в /var/www/site/errors/50x.html.
Плюс подхода — он не зависит от приложения. Если backend полностью недоступен, Nginx всё равно сможет показать страницу 502 или 503. Минус — статический файл не знает, кто пользователь, какой заказ не обработан и какой JSON нужен клиенту.
2. Named location для единого обработчика
Вместо нового URI можно направить ошибку в именованный location. Такой вариант удобен, когда нужно добавить заголовки, выбрать формат ответа или передать запрос во внутреннюю логику Nginx.
server {
listen 80;
server_name example.com;
proxy_intercept_errors on;
error_page 404 = @not_found;
error_page 500 502 503 504 = @server_error;
location / {
proxy_pass http://app_backend;
}
location @not_found {
default_type application/json;
add_header Cache-Control "no-store" always;
return 404 '{"error":"not_found"}';
}
location @server_error {
default_type application/json;
add_header Cache-Control "no-store" always;
return 503 '{"error":"service_unavailable"}';
}
}
Синтаксис = перед named location сохраняет код, который был выбран для ошибки, если обработчик не возвращает другой статус. В примере обработчик явно отдаёт 503 для группы ошибок backend.
Такой подход полезен для API, но требует дисциплины. Если этот же server обслуживает браузерные страницы, JSON-ответ на HTML-запрос будет плохим UX. Разделяйте API и веб-интерфейс по location или по отдельным виртуальным хостам.
3. Передача ответа приложения без перехвата
Иногда Nginx вообще не должен менять тело и статус. Например, приложение возвращает структурированный JSON с кодом ошибки, идентификатором запроса и машинным полем type. В этом случае не включайте proxy_intercept_errors.
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Request-ID $request_id;
}
}
Ответ 404 или 422 от API пройдёт к клиенту вместе с телом приложения. А ошибки, которые Nginx создаёт сам, например 502 при невозможности соединиться с upstream, по-прежнему можно обработать директивой error_page.
Цена этого решения — зависимость от backend. Если приложение отвечает медленно, падает или возвращает некорректный Content-Type, Nginx не исправит ситуацию. Для публичного API это обычно приемлемо только при наличии тестов на формат ошибок.
Сравнение подходов
| Подход | Что получает клиент | Сильная сторона | Основной риск |
|---|---|---|---|
Статический error_page | Готовый HTML-файл | Работает даже при падении приложения | Нет контекста запроса и бизнес-данных |
| Named location | Ответ, сформированный Nginx | Единый JSON или HTML-шаблон для группы ошибок | Можно случайно смешать форматы API и сайта |
| Без перехвата upstream | Оригинальный статус и тело приложения | Сохраняется контракт API | Ошибки backend становятся зависимыми от его доступности |
Как выбрать вариант
Выбор можно сделать по источнику ошибки и формату клиента:
- Для 502, 503 и 504 используйте статическую страницу, если приложение может быть полностью недоступно.
- Для API с формальным JSON-контрактом либо не перехватывайте ответы upstream, либо направляйте их в отдельный JSON-обработчик.
- Для обычного сайта выбирайте статический HTML или named location, если страницу нужно собирать единообразно.
- Для 404, которые описывают бизнес-сущность, например «товар не найден», чаще подходит ответ приложения, а не общий файл Nginx.
Не пытайтесь одной директивой решить все случаи. Ошибка соединения с upstream и отсутствие товара — разные события, хотя оба могут выглядеть как HTTP-ошибка.
Что ломается на практике
Перехват включили, но приложение всё равно отдаёт свой ответ
Проверьте, что proxy_intercept_errors on; находится в том же или наследуемом контексте, где работает proxy_pass. Директива относится к проксируемым ответам и не заменяет сам error_page: для нужного статуса должен существовать обработчик.
Страница ошибки возвращает 404
Чаще всего Nginx не находит файл по сочетанию root и URI. Проверьте полный путь на диске и разрешения пользователя, под которым работает worker. Если файл должен быть доступен только как внутренний обработчик, оставьте internal, но не закрывайте сам файл от процесса Nginx.
Вместо JSON приходит HTML
Это происходит, когда API попадает в общий error_page сайта. Разведите конфигурацию по location, named location или server-блокам и явно задайте default_type application/json для JSON-обработчика.
Клиент получил не тот статус
Проверьте форму директивы. Запись вроде error_page 404 /errors/404.html; делает внутренний переход, а вариант с кодом после знака равенства позволяет изменить возвращаемый статус. Отдельно учитывайте, что внутренний переход на URI запускает новый поиск location.
Обработчик зациклился
Ошибка возникает, когда сама страница ошибки снова попадает под тот же маршрут или возвращает новый статус с повторной переадресацией. Не направляйте error_page на обычный проксируемый маршрут без необходимости. Для служебных файлов используйте точный location и internal, а цепочки внутренних переходов держите короткими.
Граничные случаи и цена решения
Директива error_page меняет метод запроса на GET при внутреннем переходе на URI для методов, отличных от GET и HEAD. Это важно для POST-запроса: повторно отправлять его тело в обработчик ошибки нельзя считать безопасным по умолчанию. Если нужно сохранить логику маршрута, изучайте named location и отдельно проверяйте поведение конкретного API.
Ещё один риск — кеширование. Страница 404 для отсутствующего ресурса может кешироваться иначе, чем 503 при аварии backend. Заголовки Cache-Control задавайте осознанно, особенно для ответов с персональными данными.
Не смешивайте обработку ошибок с повторной отправкой запроса на другой upstream. Механизмы proxy_next_upstream решают задачу выбора следующего backend, а error_page — формирование ответа клиенту. Это связанные, но разные уровни.
Что проверить после настройки
Сначала проверьте каждый источник ошибки отдельно: несуществующий URI, ответ приложения 404, остановленный backend и тайм-аут соединения. Для каждого случая зафиксируйте статус, Content-Type, тело ответа и запись в error log.
- Проверить обычный URL, который должен вернуть 200.
- Проверить статический 404 и убедиться, что служебный URL нельзя открыть напрямую.
- Остановить upstream и проверить, что 502 или 504 получают ожидаемый формат.
- Отправить API-запрос с ошибкой приложения и убедиться, что JSON-контракт не заменяется HTML.
- Проверить POST-запрос: тело не должно неожиданно повторяться после внутреннего перехода.
Следующим шагом добавьте в мониторинг долю 5xx отдельно для Nginx и upstream. Один общий график показывает только симптом, а не место отказа. Подробные правила директив смотрите в официальной документации Nginx для error_page и proxy_intercept_errors.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Nginx documentation: error_page, Nginx documentation: proxy_intercept_errors
Частые вопросы
Нужно ли включать proxy_intercept_errors для ошибок 502 и 504?
Почему error_page не меняет ответ приложения с кодом 404?
Что выбрать для REST API: статическую страницу или named location?
Можно ли направить error_page на обычный проксируемый URL?
Почему после error_page POST превращается в GET?
Что делать, если у меня другая версия Nginx?
Какие подводные камни важны в production?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.