JavaRush /Курсы /Java Server /Проверка API: /health

Проверка API: /health

Java Server
22 уровень , 4 лекция
Открыта

1. Критерии smoke‑проверки локального API

Когда вы впервые поднимаете сервер, мозг радостно кричит: «Ура, процесс живой, значит API готово!» — и тут же пытается перейти к следующей теме. Но backend‑реальность жестока: процесс может жить, а HTTP‑контракт при этом быть мёртвым, странным или отвечать вообще не на тот путь. Поэтому мы вводим простое правило: результат дня измеряется запросом и ответом, а не фактом запуска.

Контракт /health уже собран на стороне сервера. Теперь важно не поверить своему коду на слово, а проверить его глазами клиента: status, headers и body должны совпасть с тем, что мы обещали.

В нашем случае smoke‑проверка должна ответить на вопрос: «Если я отправлю GET /health, получу ли я предсказуемый ответ, который можно прочитать глазами и проверить инструментом?» Здесь важно смотреть на три части ответа вместе: status code, headers и body. Если проверять только body, вы легко пропустите ситуацию, когда сервер возвращает JSON, но с неправильным Content-Type, или отвечает 500, но в body всё равно «что-то похожее на JSON». Если проверять только статус, можно получить 200 OK с пустым телом из-за ошибки записи body.

Чтобы было проще держать критерии в голове, полезно иметь маленькую «памятку» (не для экзамена, а для жизни).

Что проверяем Ожидание для /health Где видно лучше всего Если не так — что это обычно значит
Доступность по адресу http://host:port/health открывается браузер / curl / Postman сервер не стартовал, не тот порт, порт занят, неверный host
HTTP‑метод мы делаем GET curl -v, Postman, HttpClient не тот метод (или сервер ожидает другой, но это уже баг контракта)
Status code 200 curl -i, Postman, HttpClient handler не сработал, исключение, неверная регистрация context
Content-Type application/json curl -i, Postman забыли заголовок или записали «как получится»
JSON body {"status":"UP","appName":"..."} браузер (грубо), Postman, HttpClient не сериализовали DTO, неверная кодировка, не записали body

Если эта smoke‑проверка проходит, доказан только skeleton: сервер умеет принять запрос и отдать JSON. Как только путей станет больше одного, придётся уже руками различать метод, path, query и body.

Обратите внимание: это не «тестирование» в смысле отдельной дисциплины. Это дисциплина проверки контракта. Ровно то же вы будете делать в прод‑жизни, только вместо /health будет «почему у клиентов 502», вместо curl — лог‑агрегатор, и вместо спокойствия — кофе, который вы пьёте не потому что хочется, а потому что так надо.

2. Проверка через браузер

Браузер — это самый быстрый способ почувствовать «сервер вообще отвечает или я разговариваю с розеткой». И да, он удобен: вы просто открыли вкладку, ввели адрес и увидели ответ. Но браузер, как любой «удобный инструмент», часть деталей прячет — именно те, которые backend‑разработчику часто важны. Поэтому браузер хорош как первый шаг, но плох как единственный.

Если сервер у нас поднят на localhost:8080, мы открываем:

http://localhost:8080/health

И ожидаем увидеть JSON, примерно такой:

{"status":"UP","appName":"readlater-starter"}

Если вы увидели что-то похожее на JSON — это уже неплохо. Если браузер пишет This site can’t be reached или ERR_CONNECTION_REFUSED, это означает, что до HTTP‑ответа мы даже не дошли. Здесь чаще всего виноват не handler, а адрес: сервер не поднят, порт другой, порт занят, или вы запускаете сервер на одном host, а обращаетесь к другому.

Чтобы мозг не начинал магически «чинить код», полезно сделать короткую паузу и задать себе вопрос: «Я точно стучусь туда, куда сервер сказал в startup‑логе?» Это звучит как капитан‑очевидность, но именно такие капитаны спасают проекты.

Если хочется увидеть больше деталей прямо в браузере, можно открыть DevTools → вкладку Network и посмотреть status code и response headers. Это уже почти взрослый подход, но в учебном сценарии проще и быстрее перейти к curl или Postman: там детали видны сразу, без танцев по вкладкам.

3. Проверка через curl

curl — это как фонарик для HTTP‑контракта: некрасиво, зато честно. Он не пытается «помочь» вам красивой страничкой и не прячет заголовки, если вы явно попросили их показать. Для backend‑разработчика это один из лучших инструментов для smoke‑проверок, потому что он сразу даёт ответ на три ключевых вопроса: «какой статус? какие заголовки? какое тело?».

Самая полезная команда для начала — попросить curl показать и заголовки, и тело:

# -i: вывести заголовки ответа вместе с телом
curl -i http://localhost:8080/health

Ключ -i означает: «покажи response headers». Пример ожидаемого результата может выглядеть так (формат слегка зависит от версии и окружения, но смысл один):

HTTP/1.1 200 OK
Date: Thu, 19 Mar 2026 12:00:00 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 45

{"status":"UP","appName":"readlater-starter"}

Здесь нас интересуют две вещи. Первая — статус 200 OK. Вторая — Content-Type: application/json; charset=utf-8. Да, даже на /health. Потому что мы заранее договорились: наш локальный API общается в JSON, и даже технический endpoint придерживается того же стиля. Это не прихоть — это дисциплина контракта.

Если вы хотите отдельно проверить только status code (например, когда тело большое, а вы не хотите его каждый раз читать), можно попросить curl вывести только код:

# -s: без прогресс-бара, -o /dev/null: не печатать тело, -w: вывести только код статуса
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/health

Если всё хорошо, вы увидите:

200

И это приятно. (Тут можно на секунду улыбнуться и продолжить.)

Иногда полезно посмотреть запрос/ответ чуть более «болтливо», чтобы понять, дошли ли мы вообще до сервера и что происходило по пути:

# -v: подробный вывод (соединение и технические детали запроса/ответа)
curl -v http://localhost:8080/health

Флаг -v (verbose) покажет детали соединения. Если у вас проблема уровня «сервер не слушает порт», вы увидите это прямо в выводе.

Отдельная микропроверка, которая часто помогает заметить забытый Content-Type, — попросить вывести только заголовки:

curl -I http://localhost:8080/health

Это делает HEAD‑запрос, а не GET. И здесь важный момент: в рамках нашего дня мы фокусируемся на GET /health, так что -I полезен именно как быстрый просмотр заголовков, но не как «контрактная проверка метода». Если вы пока не обрабатывали HEAD, не удивляйтесь, что поведение отличается. Не усложняйте: для «правильной» проверки остаёмся на GET через curl -i.

4. Проверка через Postman

Postman в этом курсе — не просто «кнопка Send». Это ваша привычка фиксировать контракт и smoke‑сценарий так, чтобы вы могли повторить проверку через неделю, не вспоминая, какой был порт и какой путь. И да, звучит как «лишняя бюрократия», пока проект маленький. Но ровно эта бюрократия потом экономит часы.

Здесь есть два принципа, которые делают Postman‑проверку взрослой, а не случайной:

Первый принцип — использовать переменную окружения для baseUrl. Например, завести environment Local и переменную:

baseUrl = http://localhost:8080

А запрос хранить как:

GET {{baseUrl}}/health

Тогда, если вы поменяли порт в application.properties или через env var, вы не правите 10 запросов — вы правите одно значение.

Второй принцип — сохранить запрос в коллекцию. Например, в коллекцию ReadLater Local API и папку Tech. Названия не должны быть идеальными, но они должны быть понятными будущему вам. «New Request 47» — это всегда ловушка: сегодня вы помните, что это /health, а через пару дней у вас будет «археология Postman».

Когда вы отправляете запрос, проверьте глазами три вещи, как мы и договорились: статус (в Postman он виден сверху), заголовки (Headers в ответе) и JSON body. В идеале body будет сразу красиво форматирован как JSON. Если нет — это намёк, что Content-Type не application/json, и Postman не понял, что это JSON.

Есть маленький психологический трюк: в Postman удобно держать вкладку Tests пустой и не превращать её в тестовый фреймворк (мы не в testing‑курсе). Но можно сделать себе «ручной чек»: один взгляд на статус, один — на Content-Type, один — на body. Через неделю вы будете делать это автоматически. Как чистить зубы. Только вместо зубов — endpoint-ы.

5. Проверка через JDK HttpClient

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

Важная оговорка: мы сейчас не строим тестовую инфраструктуру и не пишем полноценные автотесты. Мы делаем микро‑самопроверку, которую можно запустить вручную, когда у вас что-то не отвечает, а Postman «вроде бы показывает странное».

Вот минимальный пример запроса к /health через HttpClient. Представьте, что это маленький отдельный класс (или временный метод), который вы запускаете вручную.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

// Создаём клиента один раз (для микро-проверки достаточно дефолтного)
HttpClient client = HttpClient.newHttpClient();

// Собираем GET-запрос на наш локальный endpoint /health
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8080/health"))
        .GET()
        .build();

// Отправляем запрос и получаем ответ строкой (нам нужно прочитать body глазами)
HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());

// Минимальная контрактная проверка: статус должен быть 200
System.out.println("status=" + response.statusCode()); // status=200

Этот кусочек уже даёт главное: статус. Но по нашему контрактному подходу хочется проверить и Content-Type, и кусочек body. Сделаем компактную «проверку здравого смысла», не превращая её в лабораторную по тестированию:

import java.util.Optional;

// Достаём Content-Type (может отсутствовать, поэтому Optional)
Optional<String> contentType = response.headers()
        .firstValue("Content-Type");

// Проверяем три части контракта: статус, заголовок и содержимое body
boolean ok = response.statusCode() == 200
        && contentType.orElse("").contains("application/json") // допускаем charset в конце
        && response.body().contains("\"status\":\"UP\"");      // быстрый smoke-чек по body

System.out.println("health ok=" + ok); // health ok=true

Если ok=false, вы сразу понимаете, что именно не совпало: статус, заголовок или body. И вот здесь наступает приятный момент backend‑мышления: вы больше не «чувствуете», что что-то не так. Вы знаете, какая часть контракта сломалась.

Если вы уже используете SLF4J в проекте (а вы его используете), можно заменить println на логирование, но в этом микро‑фрагменте println допустим как локальный диагностический вывод. Главное — не превращать это в основной operational‑инструмент приложения. Дневник — это хорошо, но строить по нему самолёт — сомнительно.

6. Диагностика проблем с /health

Когда /health не работает, у новичка часто включается режим «сломался код handler-a». Это понятная реакция: код же только что писали, значит он виноват. Но в server‑мире есть два больших класса проблем, и лечатся они по-разному. Если вы научитесь разделять их в голове, вы сэкономите себе много нервов.

Первый класс — сервер не поднялся или недоступен по адресу. Симптомы обычно такие: браузер не открывает страницу, curl пишет Connection refused, HttpClient бросает ConnectException. Тут нет смысла смотреть на JSON, потому что вы даже не дошли до HTTP‑ответа.

Второй класс — сервер поднялся, но /health отвечает не так, как мы договорились. Симптомы: статус не 200, или Content-Type не application/json, или body пустой/не JSON. Тут уже нужно смотреть на handler и на то, как формируется ответ.

Чтобы эти два класса проблем не путались в голове, полезно иметь короткую блок‑схему. Не потому что «мы любим диаграммы», а потому что мозг любит чёткие развилки.

flowchart TD
    A["Делаем GET /health"] --> B{"Есть ответ (HTTP response)?"}
    B -- "Нет" --> C["Проверяем startup-log: host/port"]
    C --> D["Проверяем порт занят / правильный порт / правильный host"]
    B -- "Да" --> E{"status == 200?"}
    E -- "Нет" --> F["Смотрим handler: sendResponseHeaders, исключения, context /health"]
    E -- "Да" --> G{"Content-Type содержит application/json?"}
    G -- "Нет" --> H["Проверяем exchange.getResponseHeaders().set(...)"]
    G -- "Да" --> I{"Body содержит status=UP?"}
    I -- "Нет" --> J["Проверяем ObjectMapper, запись в responseBody, закрытие потока"]
    I -- "Да" --> K["Готово: /health работает по контракту"]

Теперь — как применять это на практике, не превращая жизнь в чтение схем.

Когда «ответа нет»: сверяемся со startup‑логом и адресом

Если вы видите Connection refused, первое, что вы делаете — не открываете IDE и не переписываете handler. Вы смотрите в лог старта сервера. Он должен быть примерно такого вида:

Local API started: http://localhost:8080, appName=readlater-starter

Если в логе 8081, а вы стучитесь в 8080 — всё, диагностика закончилась за 3 секунды.

Если порт верный, но ответа нет, есть классическая причина: порт занят. Тогда на старте сервера вы обычно получите BindException (например, «Address already in use»). И это не проблема /health, это проблема того, что вы пытаетесь слушать порт, который уже занят другим процессом. Иногда этим процессом бывает ваш же сервер, который вы забыли остановить. И да, это та самая реальность разработки, где вы иногда боретесь не с кодом, а с собой из прошлого, который «оставил сервер работать и ушёл пить чай».

Когда «ответ есть, но не тот»: context, status и заголовки

Если curl -i показывает, что статус не 200, это значит, что handler либо не отработал как надо, либо отработал и отправил другой статус, либо вообще не тот handler поймали.

Самая частая причина на нашем уровне — неправильно зарегистрировали context. Например, в коде написали:

// Внимание: опечатка в пути — handler будет висеть на /heath, а не на /health
server.createContext("/heath", exchange -> handleHealth(exchange, config)); // опечатка

а в запросе вы стучитесь в /health. Сервер в таком случае может вернуть 404 (или что-то дефолтное), и вы будете смотреть на handler и думать «почему же он не вызывается».

Ещё одна частая причина — забыли выставить Content-Type. Тогда статус может быть 200, body — JSON, но клиент (и Postman) будут вести себя странно, потому что по контракту это «текст», а не JSON.

И, наконец, бывает «тишина» из-за того, что вы не закрыли response body stream. В вашем коде правильно делать это через try-with-resources:

// try-with-resources гарантирует закрытие потока и "доталкивание" ответа клиенту
try (OutputStream os = exchange.getResponseBody()) {
    os.write(body); // пишем байты body в ответ
}

Если поток не закрывать, поведение может стать нестабильным: ответ может не уйти полностью или клиент будет ждать «конец ответа» дольше, чем вы ожидаете.

Минимальный request‑лог: проверяем, что запрос дошёл

Есть очень простой приём, который почти всегда помогает: в начале handler-a залогировать метод и путь. Это не про «наблюдаемость продакшна», это про «чтобы вы не гадали».

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// Отдельный логгер под входящие HTTP-запросы, чтобы в логах было легче искать
private static final Logger log =
        LoggerFactory.getLogger("HttpRequests");

// Логируем минимум: метод и путь (этого достаточно для диагностики "доходит/не доходит")
log.info("Incoming request: {} {}",
        exchange.getRequestMethod(),
        exchange.getRequestURI().getPath());

Если вы видите в логах эту строку — запрос дошёл до handler-a. Если не видите — значит, проблема на уровне адреса/контекста, а не на уровне JSON‑сериализации.

7. Типичные ошибки проверки /health

Ошибка №1: считать успехом «процесс запустился», а не «endpoint отвечает».
Очень легко перепутать «я вижу лог Server started» с «API работает». Но клиенту абсолютно всё равно на ваш лог — он живёт в мире request/response. Поэтому всегда делайте реальный HTTP‑запрос: браузер, curl, Postman или HttpClient. Любой. Но настоящий.

Ошибка №2: проверять только body и игнорировать статус и заголовки.
Новички часто смотрят только на JSON в ответе и радуются: «Ну вот же, статус UP». А потом оказывается, что статус 500, а JSON — это просто кусок текста, который успел записаться до ошибки. Или Content-Type забыли, и часть клиентов начинает воспринимать ответ как обычную строку. Три элемента контракта — status, headers, body — живут вместе.

Ошибка №3: стучаться не на тот host/port и чинить handler вместо конфигурации.
Это классика: вы поменяли server.port в application.properties, сервер честно поднялся на новом порту, лог это подтвердил, а вы по привычке продолжаете ходить на старый адрес. Через 10 минут вы уже готовы «переписать сервер», хотя достаточно просто посмотреть на startup‑лог и поверить ему.

Ошибка №4: забыть, что http:// и https:// — разные миры.
На локальном HttpServer в этом курсе вы поднимаете обычный HTTP, без TLS. Поэтому запросы должны быть вида http://localhost:8080/health. Если вы по привычке пишете https://..., клиенты могут выдавать странные ошибки, и это будет выглядеть как «сервер сломан». На самом деле вы просто постучались в дверь не тем ключом.

Ошибка №5: не отличать «сервер недоступен» от «endpoint отвечает неправильно».
Connection refused — это не «неправильный JSON». Это «нет соединения». Это разные классы проблем, и лечатся они по-разному. В первом случае вы проверяете startup‑лог, порт, занят ли он, и жив ли процесс. Во втором — вы проверяете context, handler, sendResponseHeaders, Content-Type и запись body.

1
Задача
Java Server, 22 уровень, 4 лекция
Недоступна
Bash smoke-скрипт для `/health`
Bash smoke-скрипт для `/health`
1
Задача
Java Server, 22 уровень, 4 лекция
Недоступна
Локальный smoke-клиент на JDK `HttpClient`
Локальный smoke-клиент на JDK `HttpClient`
1
Опрос
HTTP Сервер, 22 уровень, 4 лекция
Недоступен
HTTP Сервер
Запуск и обработка запросов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ