Сервер отвечает 200 OK. Сайт не работает. Почему мониторинг ничего не заметил?

—

от автора

Вчера я разработал новый интересный кейс для моего тренажера и с интересом вам расскажу о нем:

Представим довольно неприятную ситуацию.

После очередного деплоя пользователи начинают жаловаться, что сайт перестал работать. Открывают страницу — белый экран. Никаких сообщений, ошибок или признаков жизни.

Проверяем сервер:

systemctl status nginx

Nginx работает. В журналах нет ничего необычного.

Проверяем HTTP:

curl -I https://example.com

Получаем:

HTTP/2 200content-type: text/html

CPU не перегружен, память свободна, сервер отвечает. Мониторинг тоже показывает зелёный статус.

Только вот сайт по-прежнему не работает.

Я решил разобрать такую ситуацию на небольшом тестовом стенде. Оказалось, что получить 200 OK от полностью неработающего frontend довольно легко.

И причин для этого как минимум несколько.

Почему HTTP 200 ещё ничего не гарантирует

Начнём с простого.

Когда мы выполняем:

curl -I https://example.com

мы проверяем ответ сервера на HEAD-запрос.

Если сервер возвращает 200, это означает, что запрос успешно обработан на уровне HTTP.

Но браузеру для отображения современного веб-приложения зачастую недостаточно одного HTML-файла.

Допустим, у нас обычное SPA-приложение.

Его HTML выглядит примерно так:

<!DOCTYPE html><html lang="ru"><head>  <meta charset="UTF-8">  <title>Dashboard</title></head><body>  <div id="app"></div>  <script type="module" src="/assets/app.abc123.js"></script></body></html>

Сам HTML практически пустой. Интерфейс формируется JavaScript-кодом после загрузки страницы.

Если JavaScript не загрузится, браузер вполне может показать белый экран.

При этом Nginx продолжит совершенно спокойно отвечать 200 OK на запрос главной страницы.

С точки зрения веб-сервера всё нормально.

С точки зрения пользователя — приложение мертво.

Ситуация №1. Nginx возвращает HTML вместо JavaScript

Допустим, после деплоя у нас получилась следующая структура:

/srv/www/app/├── index.html└── assets/    └── app.def456.js

Но в index.html осталась ссылка на старую версию:

<script type="module" src="/assets/app.abc123.js"></script>

Файла app.abc123.js больше нет.

Посмотрим на упрощённую конфигурацию Nginx:

server {    listen 80;    server_name example.com;    root /srv/www/app;    index index.html;    location / {        try_files $uri $uri/ /index.html;    }}

Для SPA это довольно распространённая настройка. Если пользователь открывает /dashboard, а такого файла нет, Nginx возвращает index.html. Дальше маршрутизацией занимается JavaScript.

Но здесь есть нюанс.

Запрос к отсутствующему /assets/app.abc123.js тоже попадает под это правило.

Nginx не находит файл и вместо JavaScript возвращает index.html.

Проверяем:

curl -i http://localhost/assets/app.abc123.js

Результат будет примерно таким:

HTTP/1.1 200 OKContent-Type: text/html<!DOCTYPE html><html lang="ru">...

И вот оно.

Запрос JavaScript-файла вернул 200 OK. Но в ответе вообще нет JavaScript.

Открываем DevTools в браузере и смотрим Console.

Там можно увидеть:

Failed to load module script:Expected a JavaScript module scriptbut the server responded with a MIMEtype of "text/html".

Браузер ожидал JavaScript-модуль, а получил HTML.

Он отказывается его выполнять.

Как исправить

Для начала нужно восстановить согласованность файлов деплоя: index.html должен ссылаться на реально существующий JavaScript-бандл.

А чтобы Nginx не скрывал подобную ошибку, можно отдельно обрабатывать статические ресурсы:

location ^~ /assets/ {    try_files $uri =404;}

Теперь при запросе отсутствующего JS-файла сервер вернёт честный 404 Not Found.

Это не починит сломанную сборку автоматически, зато сделает проблему гораздо заметнее.

И мониторингу, и разработчику будет проще понять, что ресурс действительно отсутствует.

Ситуация №2. Файл существует, но Nginx ищет его не там

Теперь представим другой вариант.

Файл JavaScript на сервере есть. Путь в HTML правильный.

Но браузер всё равно его не получает.

В конфигурации обнаруживается:

location /assets/ {    alias /srv/www/app/;}

А реальные файлы находятся в:

/srv/www/app/assets/

Проблема в том, как работает alias.

При таком правиле запрос:

/assets/app.js

Nginx попытается сопоставить с:

/srv/www/app/app.js

А нужный файл лежит в другой директории.

Исправляем путь:

location /assets/ {    alias /srv/www/app/assets/;}

Затем проверяем конфигурацию:

sudo nginx -t

Если всё нормально:

sudo systemctl reload nginx

И повторяем запрос.

Это кажется довольно очевидной ошибкой, когда уже знаешь причину.

Но в момент расследования её легко пропустить: файл на месте, Nginx работает, главная страница открывается.

Нужно проверять не только наличие файла на диске, но и то, каким путём Nginx пытается его найти.

Ситуация №3. JavaScript есть, но браузер отказывается его выполнять

Третий случай ещё интереснее.

Файл существует. Nginx его находит. HTTP-ответ — 200.

Но приложение не запускается.

Проверяем заголовки:

curl -I http://localhost/assets/app.js

Получаем:

HTTP/1.1 200 OKContent-Type: text/plain

На первый взгляд ничего страшного.

Но для JavaScript-модулей браузеры применяют строгую проверку MIME-типа.

Если сервер возвращает JavaScript как text/plain, модуль не будет выполнен.

Например, такая проблема может появиться из-за неправильной конфигурации MIME-типов:

location /assets/ {    types { }    default_type text/plain;}

Здесь мы фактически заставляем Nginx отдавать ресурсы с неподходящим типом содержимого.

В нормальной конфигурации необходимо использовать корректные MIME-сопоставления, обычно подключаемые через:

include /etc/nginx/mime.types;

При этом нужно убедиться, что локальные настройки types не переопределяют нужные сопоставления.

После исправления заголовок JavaScript-файла должен содержать подходящий MIME-тип, например:

Content-Type: text/javascript

И браузер сможет выполнить модуль.

В очередной раз: сервер работал, файл существовал, HTTP возвращал 200. Но сайт оставался неработоспособным.

А почему мониторинг ничего не заметил?

Теперь вернёмся к исходной ситуации.

Допустим, наш мониторинг каждые 30 секунд делает:

curl -fsS -o /dev/null https://example.com/

Если сервер возвращает успешный HTTP-код, проверка завершается без ошибки.

Для простых сайтов этого иногда достаточно.

Но для SPA-приложения подобная проверка говорит только о доступности HTTP-страницы.

Она не отвечает на несколько важных вопросов:

  • Загрузились ли JavaScript-ресурсы?

  • Выполнился ли код приложения?

  • Появился ли интерфейс?

  • Может ли пользователь выполнить основное действие?

Получается, что мы проверяем доступность Nginx, а называем это доступностью приложения.

Как я бы улучшил проверку

Я бы разделил мониторинг на несколько уровней.

Первый — доступность сервера.

Проверяем, что Nginx отвечает и главная страница доступна.

Второй — доступность ресурсов.

Проверяем, что JavaScript-файлы действительно существуют, имеют корректный MIME-тип и возвращают ожидаемое содержимое.

Третий — работоспособность приложения.

Здесь уже полезен браузерный smoke-тест.

Например, с Playwright можно открыть страницу в Chromium и проверить, что приложение действительно отрисовало нужный элемент.

Условный пример:

import { chromium } from 'playwright';const browser = await chromium.launch();try {  const page = await browser.newPage();  await page.goto('https://example.com/');  await page.locator('[data-testid="dashboard"]')    .waitFor({ state: 'visible', timeout: 10000 });  console.log('Frontend is working');} finally {  await browser.close();}

Разумеется, в самом приложении должен существовать соответствующий элемент.

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

Она не просто спрашивает у Nginx, жив ли он.

Она проверяет, что интерфейс действительно смог загрузиться.

Я бы не запускал полноценный браузерный тест каждые несколько секунд на каждом экземпляре сервиса. Но для проверки после деплоя и периодического внешнего мониторинга это полезный дополнительный уровень.

Что в итоге

У всех трёх ситуаций есть одна общая черта.

HTTP 200 означает успешный ответ на конкретный HTTP-запрос, но не гарантирует, что приложение работает.

Можно получить 200 от главной страницы, 200 от отсутствующего JavaScript-файла, 200 от ресурса с неправильным MIME-типом.

И при этом пользователь увидит только белый экран.

Меня в подобных задачах интересует даже не сама ошибка, а разница между тем, что показывает мониторинг, и тем, что происходит на самом деле.

Мы привыкли проверять службы, порты и HTTP-коды. Но иногда все эти показатели зелёные, а продукт уже недоступен.

Наверное, поэтому хороший healthcheck должен отвечать не только на вопрос «жив ли сервер», но и на вопрос «может ли пользователь сделать то, ради чего вообще существует этот сервис».

ссылка на оригинал статьи https://habr.com/ru/articles/1105818/