1. Алгоритм разбора HTTP-вызова
К этому месту уже видно четыре разных источника боли: запрос может быть несамодостаточным, операция может менять ресурс, ответа может не быть, а пришедший ответ ещё не обязан совпадать с договором. Теперь полезно собрать эти куски в один рабочий порядок разбора, чтобы не диагностировать HTTP по интуиции и не чинить не ту проблему. Инструмент потом может спрятать часть boilerplate, но причины поломки всё равно раскладываются по тем же вопросам.
Когда что-то пошло не так, мозг новичка обычно делает героический прыжок в середину истории: «О! В body какая-то странная строка», «О! Прилетело 404 — значит сервер умер», «О! Исключение — значит баг в бизнес-логике». Проблема в том, что это не анализ, а гадание, просто с более красивыми терминами.
Backend-мышление любит порядок, потому что порядок экономит время. Если вы каждый раз разбираете проблемы по одной схеме, вы быстрее находите причину, меньше ругаетесь на провайдера (иногда зря), и реже «чините не то». Это особенно важно для ReadLater Starter: как только приложение ходит во внешний каталог книг, приходится отличать «сервис ответил ошибкой» от «мы не получили ответа» и от «ответ пришёл, но это не то, что обещал контракт».
Если коротко: алгоритм нужен не потому, что вы не умные, а потому что вы живой человек. А живым людям полезны чек-листы, иначе они начинают дебажить по гороскопу.
Три вопроса: ответ, статус, контракт
Полезно держать в голове три вопроса, которые нужно задавать строго в порядке. Этот порядок важнее, чем кажется: он защищает от самой частой ошибки — «сначала полезли в body, а потом выяснили, что ответа вообще не было». Схема простая, почти как «посмотри, включён ли компьютер в розетку», только в HTTP.
Первый вопрос: ответ вообще был? Если ответа не было, то вы не можете анализировать статус-код по определению — его нет. Второй вопрос: если ответ был, какой статус? Статус — это быстрый ярлык смысла: успех, ошибка запроса, отсутствие ресурса, конфликт, внутренняя ошибка. Третий вопрос: если статус успеха, соответствует ли ответ контракту? Потому что 200 OK — это ещё не гарантия, что вам прислали то, о чём договаривались.
Вот это можно представить как очень простое дерево решений:
flowchart TD
A["Проблемный HTTP-вызов"] --> B{Ответ получен?}
B -- Нет --> C["Сетевой сбой / нет ответа"]
B -- Да --> D{"Status code"}
D -- 4xx/5xx --> E["Получен error response"]
D -- 2xx --> F{Контракт выполнен?}
F -- Нет --> G["Нарушение контракта"]
F -- Да --> H["Успех"]
Обратите внимание на важную психологическую вещь: 404 находится в ветке «ответ получен». Это значит, что сервер жив, сеть доставила запрос и вернула response. Это уже полезная информация, и она кардинально отличается от ситуации «нет ответа вообще».
2. Шаг 1 — проверка наличия ответа
Начинать разбор нужно с самого скучного, но самого спасительного вопроса: «Ответ был?» Скучно — потому что хочется сразу читать body и искать «что там не так». Спасительно — потому что иногда никакого HTTP-response не существует, и вы пытаетесь анализировать фантом.
Если ответа нет, дальше не о чем спорить на уровне статусов: у вас нет ни 404, ни 500, ни заголовков, ни тела. Причины могут быть разными — сервис недоступен, адрес неверный, сеть отвалилась, соединение не установилось, — но ветка диагностики одна: сначала признаём, что response не получен, и только потом выясняем, почему.
Чтобы зафиксировать мысль на простом Java-примере, можно представить, что результат вызова — это либо response, либо ошибка. Сейчас мы не делаем реальный HTTP-код (он будет позже), но можем смоделировать форму результата — это полезно для мышления.
package com.example.readlater;
// Результат вызова: либо есть HTTP-ответ, либо случилась ошибка на пути к нему (сеть, DNS, таймаут и т.п.)
public record CallOutcome(RemoteResponse response, Exception error) {
// Главное в этом объекте: мы отделяем "нет ответа" от "ответ с плохим статусом".
public boolean hasResponse() {
// Если response == null — значит HTTP-ответа не было как класса (нечего анализировать по статусу/заголовкам).
return response != null;
}
}
Если hasResponse() вернул false, не надо искать «правильный статус» и тем более разбирать body: у вас другая ветка проблемы. Здесь выясняют, был ли сетевой сбой, не промахнулись ли host/port/path и не упёрлись ли во внешнюю инфраструктуру.
И здесь полезно помнить семантику операции. Для чтения отсутствие ответа обычно означает потерянный результат. Для изменяющего запроса неприятнее другое: клиент уже не знает, просто ли не дождался ответа или сервер успел что-то изменить.
3. Шаг 2 — статус как класс проблемы
Когда вы убедились, что ответ действительно получен, самое время сделать то, что многие пропускают: посмотреть на status code и воспринять его как «класс ситуации». Это не просто цифра, а договорённый сигнал между клиентом и сервером: «у меня получилось», «у тебя проблема в запросе», «ресурса нет», «конфликт», «у меня внутренний сбой».
Важный момент: error response — это тоже результат контракта. Если сервер ответил 400 или 404, это уже «нормальная форма общения»: он смог обработать ваш запрос настолько, чтобы вернуть структурированный ответ (пусть и с ошибкой). Это отличается от ситуации, когда ответа нет вообще.
Чтобы «приземлить» это на маленький код, нам нужен минимальный объект ответа. Сделаем рекорд, где есть статус, content-type и body. Да, body пока строкой: для этой диагностики нам важнее различить статус и форму ответа, чем разбирать JSON по полям.
package com.example.readlater;
// Минимальная модель HTTP-ответа для диагностики.
// Здесь важны: статус, Content-Type и тело (пока строкой, без JSON-парсинга).
public record RemoteResponse(int status, String contentType, String body) {
// 2xx — класс "успех": сервер считает, что запрос обработан.
public boolean is2xx() {
return status >= 200 && status < 300;
}
}
Теперь можно выделить очень простую проверку: «статус — это ошибка или нет».
package com.example.readlater;
public class Statuses {
// 4xx/5xx — это error response: ответ есть, но сервер сообщает об ошибке.
public static boolean isError(int status) {
return status >= 400 && status < 600;
}
}
Здесь достаточно держать одну опору: 404 и 500 уже принадлежат ветке «response получен». Это разные проблемы, но обе отличаются от ситуации, где статуса нет вообще.
Чтобы было проще держать это в голове, вот небольшая таблица-интерпретатор (без превращения лекции в справочник RFC):
| Что вы наблюдаете | Это… | Что НЕ стоит делать первым делом |
|---|---|---|
| Нет response (есть исключение/обрыв) | Сетевой сбой / недоступность | Искать «правильный статус» и разбирать body |
| Response со статусом 4xx | Ошибка запроса / клиентская проблема | Объявлять «сервис упал» (он как раз ответил) |
| Response со статусом 5xx | Ошибка на стороне сервера | Считать, что вы «точно всё правильно отправили» (иногда вы спровоцировали проблему) |
| Response со статусом 2xx | Успешная доставка ответа | Считать, что контракт точно выполнен (это следующий шаг проверки) |
В проектном контексте ReadLater Starter это будет выглядеть особенно полезно, когда мы начнём ходить во внешний каталог книг. Внешний провайдер может честно ответить 404 на неправильный путь (например, вы опечатались в URL), может ответить 400 на невалидный параметр, может ответить 500, потому что у него проблемы. И все эти три случая — ответы, а не «отсутствие ответа».
4. Шаг 3 — проверка контракта
И тут все наконец собирается: после проверки response и status остаётся проверить, действительно ли пришедший «успех» похож на то, о чём договаривались.
Сейчас будет момент, который часто ломает романтическое представление о 200 OK: иногда сервер отвечает 200, но делает это «как-то не так». И это уже не просто «ошибка запроса» и не «внутренняя ошибка» — это часто признак того, что контракт нарушен (или мы его неправильно поняли).
Контракт — это не только «поля в JSON». Это ещё и Content-Type, и ожидаемая форма ответа, и допустимые статусы для этого endpoint-а. Например, вы ожидаете JSON, а вам приходит HTML-страница с текстом «Service temporarily unavailable» — и да, иногда такое прилетает со статусом 200. Или вам приходит пустое body там, где вы ожидали объект. Или заголовки такие, что клиент не может понять, как это интерпретировать.
Давайте покажем это на очень приземлённой проверке: считаем, что «контракт похож на JSON», если Content-Type — application/json, и в body есть хотя бы ожидаемый маркер. Это не «правильная валидация», а учебная иллюстрация идеи: даже успех нужно сверять с ожиданием.
package com.example.readlater;
public class ContractChecks {
// Очень учебная проверка: похоже ли это на JSON-ответ, который мы ожидаем.
// Это НЕ "правильный" JSON-парсер. Здесь важна идея: даже при 200 контракт может быть сломан.
public static boolean looksLikeJson(RemoteResponse r) {
return "application/json".equals(r.contentType())
&& r.body() != null
// Грубый маркер: в настоящем проекте вы бы парсили JSON, а не искали "{".
&& r.body().contains("{");
}
}
Теперь представьте два ответа.
Первый — похож на то, что мы хотим:
// "Нормальный" ответ: JSON и ожидаемая структура.
RemoteResponse ok = new RemoteResponse(
200,
"application/json",
"""
{"items":[],"count":0}
"""
);
Второй — «успешный», но странный:
// "Странный" ответ: статус 200, но Content-Type и тело не про JSON API.
RemoteResponse weird = new RemoteResponse(
200,
"text/html",
"""
<html><body>Oops</body></html>
"""
);
Оба формально 200, но второй нарушает ожидание: клиент думал, что это JSON API, а ему прислали HTML. Это типичная ситуация, когда сервис стоит за прокси/страницей ошибки, или когда вы попали не туда (например, на «человеческий» сайт вместо API). И ваш анализ должен уметь сказать: «Статус успешный, но контракт не совпадает». Это отдельная категория, и она очень полезна, потому что помогает не тратить время на «почему JSON не парсится», когда на самом деле JSON там и не ночевал.
Пока нам достаточно увидеть сам факт несоответствия формы. Даже когда тело ответа станет для нас уже не просто строкой, а набором конкретных полей, порядок мысли останется тем же: сначала response, потом status, потом совпадение с договором.
Ещё один важный момент: контракт может нарушаться не только формой body, но и статусом. Например, вы ожидали, что GET /books/search всегда возвращает 200 (даже если ничего не найдено — пустой список), а сервис вдруг начал отдавать 404. С точки зрения HTTP это «возможное поведение», но с точки зрения вашего договора — это изменение правил игры. И это нужно видеть именно как «контракт поехал», а не как «ну, опять 404, значит всё пропало».
5. Мини-диагностика в коде
Сейчас мы сделаем маленький, но очень показательный шаг: превратим нашу схему в код, который классифицирует исход проблемного вызова. Это не «готовая библиотека», не «фреймворк», и даже не реальный HTTP-клиент — это учебный каркас мышления, который позже (когда появится настоящий вызов) вы просто заполните реальными данными.
Начнём с перечисления возможных итогов. Нам достаточно четырёх:
package com.example.readlater;
public enum CallVerdict {
// HTTP-ответа нет (соединение не установилось, таймаут, DNS, обрыв и т.п.)
NETWORK_FAILURE,
// HTTP-ответ есть, но статус 4xx/5xx (сервер вернул ошибку протокольно).
HTTP_ERROR_RESPONSE,
// HTTP-ответ "не похож" на то, о чём договаривались (статусы/типы/тело).
CONTRACT_VIOLATION,
// Всё хорошо: есть ответ, статус успеха, контракт совпал.
SUCCESS
}
Теперь соберём анализатор. Он будет применять ровно тот порядок, который мы обсуждали: сначала ошибка/нет ответа, потом статус, потом контракт.
package com.example.readlater;
public class CallAnalyzer {
public CallVerdict analyze(CallOutcome outcome) {
// 1) Самый важный первый вопрос: ответ вообще был?
if (!outcome.hasResponse()) return CallVerdict.NETWORK_FAILURE;
RemoteResponse r = outcome.response();
// 2) Если статус 4xx/5xx — это "error response": сервер ответил, но сообщил об ошибке.
if (Statuses.isError(r.status())) return CallVerdict.HTTP_ERROR_RESPONSE;
// 3) Всё, что не попало в 2xx и 4xx/5xx, для этой учебной схемы считаем unexpected-status веткой.
// Здесь мы сознательно не раскладываем redirect-истории и другие спецслучаи.
if (!r.is2xx()) return CallVerdict.CONTRACT_VIOLATION;
// 4) Даже при 2xx проверяем, что ответ соответствует ожиданиям (контракту).
if (!ContractChecks.looksLikeJson(r)) return CallVerdict.CONTRACT_VIOLATION;
return CallVerdict.SUCCESS;
}
}
Здесь есть намеренное упрощение: всё, что не попало в ожидаемую ветку успеха или явной ошибки, мы временно считаем unexpected для текущего контракта. Нам сейчас важен сам порядок диагностики, без отдельного разбора всех специальных случаев протокола.
Обратите внимание на намеренную простоту. Мы не делаем «красивую» иерархию исключений, не строим универсальные абстракции, не добавляем ретраи. Наша цель — чтобы у вас в голове закрепился порядок вопросов. Этот код можно читать почти как русский текст: «если ответа нет — сетевой сбой; если статус ошибочный — error response; если статус странный — контракт; если форма ответа не та — контракт; иначе успех».
Теперь можно показать, как это выглядит в мини-демо. Да, это будет «игрушка», где мы руками создаём outcome, но она отлично иллюстрирует классификацию.
package com.example.readlater;
public class ReadLaterApplication {
public static void main(String[] args) {
CallAnalyzer analyzer = new CallAnalyzer();
// Имитируем успешный ответ: ответ есть, 200, JSON-подобное тело.
CallOutcome ok = new CallOutcome(
new RemoteResponse(200, "application/json", """
{"items":[],"count":0}
"""),
null
);
System.out.println(analyzer.analyze(ok)); // SUCCESS
}
}
И ещё один пример — 404 (ответ есть, но ошибочный статус):
package com.example.readlater;
public class Demo404 {
public static void main(String[] args) {
// 404 — это НЕ "нет ответа", это "ответ есть, но сервер сообщает об ошибке запроса/ресурса".
CallVerdict v = new CallAnalyzer().analyze(
new CallOutcome(
new RemoteResponse(404, "application/json", """
{"message":"not found"}
"""),
null
)
);
System.out.println(v); // HTTP_ERROR_RESPONSE
}
}
И пример «нет ответа вообще»:
package com.example.readlater;
public class DemoNetworkFailure {
public static void main(String[] args) {
// response == null: HTTP-ответа нет, есть только исключение на стороне клиента/сети.
CallOutcome out = new CallOutcome(null, new IllegalStateException("Connection refused"));
System.out.println(new CallAnalyzer().analyze(out)); // NETWORK_FAILURE
}
}
С точки зрения развития ReadLater Starter это полезно, потому что позже у нас появится настоящий «внешний каталог книг». И когда вы будете отлаживать вызов «поиск книги», вам пригодится простая привычка: «Что это? Нет ответа? Есть ответ со статусом? Успех, но контракт сломан?» Это гораздо лучше, чем «что-то не так, давайте печатать всё подряд».
6. Типичные ошибки при разборе вызовов
Ошибка №1: анализ начинается с body.
Самая популярная ловушка: вы сразу читаете body, пытаетесь понять «почему там не то», а потом выясняется, что body — это вообще не то, что вы думаете, или его нет. Правильный порядок жёсткий: сначала выясняем, существует ли response, затем статус, и только потом тело.
Ошибка №2: 404 принимается за «сервис недоступен».
404 — это ответ живого сервиса. Да, он вам не нравится, но он означает, что сеть и сервер сработали достаточно хорошо, чтобы вернуть структурированный сигнал. Если вы называете это «недоступностью», вы автоматически начнёте искать проблему «в инфраструктуре», хотя часто это банально неправильный path или отсутствующий ресурс.
Ошибка №3: любое исключение считается 500 на стороне сервера.
Исключение на стороне клиента (или в вашем приложении) очень часто означает «нет ответа», а не «сервер вернул 500». У 500 есть конкретный признак: вы получили response и увидели статус. Если response нет — это другой класс проблем, и его надо отделять.
Ошибка №4: 200 OK автоматически означает «всё хорошо».
200 означает, что сервер сказал «успех», но не гарантирует, что вы с сервером одинаково понимаете формат ответа. Если Content-Type не тот, если body пустое, если форма «поехала», это может быть контрактная проблема. И она особенно неприятна, потому что выглядит как успех, но ломает клиента.
Ошибка №5: одинаково легко повторяются запросы чтения и изменения данных.
Если у вас проблемы с сетью, повторить GET психологически проще: чтение не должно менять состояние. А вот повторять «изменяющие» запросы (создание/обновление/удаление) нужно осторожнее, потому что при сетевом сбое вы не всегда знаете, что успел сделать сервер. Сегодня мы не строим систему повторов, но правильная осторожность должна появиться уже сейчас.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ