The Jerusalem PostDiplomatic challenges from US, Europe, Iran, await Israel's next prime minister - analysisPunchEkiti APM candidate promises 100 federal jobsDaily MaverickBHEKISISA: Robots, polony and the long road to recovery from schizophreniaInquirer EntertainmentMarian Rivera took just 1 day’s pay for ‘Balota,’ donated the rest to crew, charityInquirerResigning never crossed Sara Duterte’s mind despite impeachment, says spoxThe South AfricanBrand-new Bafana star’s playing style compared with Doctor KhumaloZDF heuteAktuelle Pressemitteilungen des ZDFFootball ItaliaNapoli eye Chelsea winger Gittens in January swoopNHK 社会JIS不適合生コン製造などで認証取り消し 麻生コンクリート工業Wirtualna PolskaGibała po marszu: "Morderca nie osiągnął swojego celu"DeadlineITV News & Guardian Closer To Publishing Sexual Misconduct Allegations About British Star After Injunction Appeal Deadline Passed20 MinutenCS, UBS – und bald 1000 Wohnungen? Uetlihof steht vor Verkauf
The Daily Newsstand · Free, Always
Tuesday, November 10, 2026

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

Translate

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

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

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

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

systemctl status nginx

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

Проверяем HTTP:

curl -I https://example.com

Получаем:

HTTP/2 200
content-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 OK
Content-Type: text/html

<!DOCTYPE html>
<html lang="ru">
...

И вот оно.

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

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

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

Failed to load module script:
Expected a JavaScript module script
but the server responded with a MIME
type 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 OK
Content-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 должен отвечать не только на вопрос «жив ли сервер», но и на вопрос «может ли пользователь сделать то, ради чего вообще существует этот сервис».

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.