1. «Нет ответа» vs «ответ с ошибкой»
Различать safe и unsafe полезно не только ради красивой семантики методов. Как только сеть начинает чудить, разница становится очень практической: если чтение не вернуло ответ, мы потеряли результат; если не ответила изменяющая операция, возникает неприятная неопределённость — успел ли сервер уже что-то поменять.
Когда мы учимся HTTP, очень легко привыкнуть к мысли: «Любой запрос заканчивается статусом». Мозг любит аккуратные истории: отправил запрос → получил 404 → сделал вывод. Но сеть не обязана быть такой вежливой. Иногда вы не получаете никакого статуса, потому что HTTP-ответ не пришёл — и это отдельный класс проблем, который нельзя «переименовать в 500 и забыть».
Представьте разницу между двумя ситуациями. В первой вы постучались в дверь, и вам открыли и сказали: «Нет, этого человека здесь нет» — это похоже на 404 Not Found: ответ есть, просто он не про успех. Во второй вы стучались, но дверь вообще не существует (или вы стучали в стену, а не в дверь) — это уже история не про статус, а про то, что ваш запрос не дошёл или ответ не вернулся. И пока вы не отделите эти два мира, вы будете ошибочно думать, что «404 значит сервис недоступен», или что «любой сбой — это 500».
Давайте зафиксируем различие в очень прикладной табличке. Она простая, но спасает нервы:
| Ситуация | Был ли HTTP-ответ? | Есть ли status code? | Пример ощущения | Что это означает |
|---|---|---|---|---|
| Успех | Да | Да | «Получил данные» | Сервер обработал запрос и ответил успешно |
| Ошибка протокола/сервера | Да | Да | «Ответ пришёл, но ошибка» | Сервер обработал запрос и сообщил об ошибке через статус |
| Сетевой сбой | Нет | Нет | «Вообще ничего не пришло» | Клиент не получил HTTP-ответ: проблема на пути до/от сервера |
| Слишком медленно | «Технически может быть», но для клиента — нет | Обычно нет | «Зависло» | Клиент прекращает ждать, потому что время ожидания вышло |
В этой лекции нас интересуют как раз последние две строки: «нет ответа» и «слишком медленно». Это не про то, какой status code выбрать. Это про то, что статус может не появиться вообще.
2. Три класса проблем: «не дошли», «не то», «поздно»
Чтобы не относиться к сетевым сбоям как к мистике («компьютер обиделся»), полезно мысленно разложить удалённый вызов на несколько этапов. Мы не будем уходить в детали TCP, TLS и прочих букв, но нам нужно понять прикладное: на каком этапе может сломаться обмен так, что HTTP-ответа не будет. Это помогает не паниковать и не искать 404 там, где его физически нет.
Упрощённо, любой удалённый HTTP-вызов можно представить как путь из трёх зон. В первой зоне запрос ещё «в дороге» (и может не доехать). Во второй зоне запрос доехал до сервера, сервер что-то сделал и сформировал ответ (успех или ошибка). В третьей зоне ответ возвращается обратно клиенту (и тоже может не доехать или прийти слишком поздно).
Вот схема без лишней физики, но достаточно честная для backend-мышления:
flowchart TD
A[Клиент сформировал запрос] --> B{"Запрос дошёл до сервера?"}
B -- нет --> X[Сетевой сбой: нет ответа]
B -- да --> C[Сервер обработал запрос]
C --> D{"Ответ успел вернуться клиенту?"}
D -- нет/слишком поздно --> Y["Медленный ответ / timeout: для клиента ответа нет"]
D -- да --> E["Клиент получил HTTP status + headers + body"]
Если вы привыкли к локальным Java-методам, эта схема поначалу кажется странной: «как это — результата нет?» Но в сети результат может не прийти не потому, что «сервер вернул плохой статус», а потому что обмен не завершился как протокол. И это главный переход от «console-мышления» к «backend-мышлению»: сначала убедись, что вообще есть ответ, и только потом обсуждай статус и body.
3. Когда ответа нет: как распознать сетевой сбой
Ситуация «нет ответа» в реальности встречается постоянно, просто в обычной жизни её прячет за вас браузер: он пишет что-то вроде “This site can’t be reached”. Но для backend-приложения такой сценарий — абсолютно нормальный рабочий кейс, а не редкое затмение. Важно научиться формулировать его правильно: мы не получили HTTP-ответ, значит у нас нет статуса, нет заголовков и нет тела ответа.
Причины могут быть разными, но нам сейчас не нужно их каталогизировать как справочник. Достаточно осознать общий смысл: сервис мог быть выключен, адрес мог быть неверным, сеть могла «чихнуть», соединение могло оборваться. Итог один — у клиента нет материала для анализа HTTP-контракта, потому что контракт предполагает обмен, а обмен не завершился.
Чтобы это закрепить на уровне кода, давайте смоделируем минимальную ситуацию в Java. Это не настоящий HTTP-клиент (он будет позже в курсе), а учебная модель: либо мы «получили ответ», либо «упали до ответа».
// Снимок того, что есть только при наличии HTTP-ответа: статус и тело.
record HttpSnapshot(int statusCode, String body) { }
class RemoteCallSimulator {
HttpSnapshot call(boolean serviceAvailable) {
// Если сервис недоступен, мы принципиально не можем "вернуть статус": ответа нет.
if (!serviceAvailable) {
throw new IllegalStateException("No response: service unavailable");
}
// Это ветка, где ответ «пришёл» и можно говорить о статусе и body.
return new HttpSnapshot(200, "OK");
}
}
Обратите внимание на ключевую мысль: в ветке недоступности нет HttpSnapshot. Некуда положить statusCode, потому что его нет. И это принципиально отличается от ситуации, где статус есть, но он, например, 404.
Теперь добавим простую функцию интерпретации, близкую к тому, как вы будете мыслить при разборе проблем:
class CallInterpreter {
String interpret(HttpSnapshot response, Exception error) {
// 1) Сначала отделяем ситуацию "ответа нет" (ошибка сети/соединения/таймаут).
if (error != null) return "ошибка сети: ответа нет";
// 2) Только если ответ есть, имеет смысл разбирать статус.
if (response.statusCode() == 404) return "ответ получен: ресурс не найден";
// 3) В остальных случаях просто показываем факт, что статус существует.
return "ответ получен: status=" + response.statusCode();
}
}
Здесь нет «правильной архитектуры» — только правильный порядок мышления. Сначала отделяем «ответа нет» от «ответ есть». И только если ответ есть, мы вообще начинаем говорить про 404, 500 и другие статусы. Это особенно важно, потому что многие новички в голове смешивают 404 (ответ пришёл) с «сервер не работает» (ответа нет). В итоге получается странная диагностика: «У нас 404, значит сервис недоступен». Нет, это значит, что сервис доступен настолько, что смог вам честно ответить.
4. Медленный ответ: время — часть вызова
Медленный ответ выглядит коварно, потому что формально «всё же работает»: где-то там сервер, возможно, даже считает ваш запрос и когда-нибудь ответит. Но с точки зрения клиента это уже может быть провалом. В backend-мире время ожидания — такой же ресурс, как память и процессор. Если вы держите поток в ожидании слишком долго, вы сжигаете время пользователя и собственные ресурсы, а иногда ещё и блокируете дальнейшую работу приложения.
Чтобы почувствовать это на практике, достаточно представить, что ваше приложение — это не один вызов, а их десятки. Если каждый второй запрос «висит» по 20 секунд, у вас быстро появится эффект: приложение не падает, но становится бесполезным. Пользователь делает запрос, а в ответ получает тишину. Формально сервер мог бы ответить… просто не в этой жизни. Поэтому в реальности мы почти всегда вводим понятие «лимита ожидания» (timeout) — но сегодня мы говорим только о смысле, без деталей настройки в реальном HTTP-клиенте.
Смоделируем идею «слишком долго» на уровне простейшей функции:
class TimeBudget {
boolean tooSlow(long responseTimeMs, long limitMs) {
// Время ответа — это часть результата вызова: если не уложились, сценарий "ломается".
return responseTimeMs > limitMs;
}
}
И ещё один маленький пример, где мы замеряем время выполнения «вызова» и решаем, что это уже не ок:
class SlowCallDemo {
String callWithTimeLimit(long limitMs, long simulatedMs) {
// Учебная имитация: мы не делаем реальный sleep, а просто считаем "как будто прошло время".
long started = System.currentTimeMillis();
long finished = started + simulatedMs;
long tookMs = finished - started;
// Если вышли за лимит, для клиента это эквивалентно "ответа нет" (он перестал ждать).
return tookMs > limitMs ? "timeout: ответа нет" : "ответ успел прийти";
}
}
Понятно, что это имитация (мы не делаем реальный sleep, чтобы не превращать лекцию в “подождите 20 секунд”). Но смысл правильный: если вызов не уложился в ваш лимит, для вас ответа как бы нет. Это психологически важно: timeout — это не «слегка медленно», это отдельный класс исхода. Вы прекращаете ждать, потому что продолжать ждать вреднее, чем признать проблему.
И ещё один важный нюанс: медленный ответ — это не всегда «сервис плохой». Иногда вы сами запросили что-то тяжёлое, иногда сеть на вашем конце слабая. С точки зрения клиента причины могут быть разными, но поведение одно: “мы не можем продолжать сценарий, потому что обмен не завершился вовремя”.
5. Недоступный сервис: отвечать всё равно тебе
Недоступность сервиса — это отдельный вид печали: вы сделали всё правильно, запрос сформировали корректно, статус-коды помните, заголовки не перепутали, а всё равно ничего не работает. И тут важно принять взрослую мысль: в backend-разработке вы почти всегда зависите от внешних систем. Они могут быть медленными, они могут упасть, они могут временно «отстрелить себе ногу» обновлением. И это не экзотика — это повседневность.
Самое полезное, что можно сделать на уровне мышления прямо сейчас: научиться говорить про такие ситуации честно. Не «у нас 500» (потому что 500 — это статус ответа, а ответа может не быть). Не «у нас 404» (потому что 404 — значит ответ пришёл). А именно: «удалённый сервис недоступен» или «удалённый сервис не ответил за разумное время». Это звучит скучно, но зато не путает вас в диагностике.
Давайте сравним несколько похожих по ощущениям сценариев, которые часто путают:
| Сценарий | Ответ пришёл? | Пример | Как думать |
|---|---|---|---|
| Сервис реально недоступен (нет ответа) | Нет | «Соединение не установлено» | Сетевой сбой: до HTTP-уровня не дошли |
| Сервис ответил, что ему плохо | Да | 500 | Ответ есть, сервер сообщил о внутренней ошибке |
| Сервис ответил, что временно не может | Да | 503 (пример) | Ответ есть, сервер честно сказал «не готов» |
| Сервис работает, но ресурс отсутствует | Да | 404 | Ответ есть, ресурс не найден |
Нам пока не важно, какие именно статусы бывают у «временно не могу» — в этой лекции мы тренируем границу: статус существует только если ответ получен. Если ответ не получен, статус «неоткуда взять». И это помогает вам не делать ложных выводов.
Когда вы будете писать наш ReadLater Starter как HTTP-клиент к внешнему каталогу, вы очень быстро столкнётесь с простым фактом: внешний сервис — это «чужая территория». Вы не можете его “починить” своим try/catch, но вы можете сделать своё приложение предсказуемым: корректно показать пользователю, что сервис недоступен, и не притворяться, что это “404” или “500”.
6. Мини-алгоритм: порядок проверок
Когда что-то ломается, руки часто автоматически тянутся читать body, смотреть “что там написано”, и искать в тексте подсказку. Но это ловушка: если ответа нет, body нет тоже. Поэтому нужно приучить себя к фиксированному порядку: сначала выясняем, есть ли ответ как факт, и только потом разбираем, что именно внутри него. Это как в медицине: сначала проверяют, есть ли пульс, а потом уже выясняют, чем человек недоволен.
Вот очень компактная схема мышления, которую стоит буквально держать в голове:
flowchart TD
A[Проблемный вызов] --> B{"Ответ получен?"}
B -- Нет --> N["Сетевой сбой / timeout: статуса нет"]
B -- Да --> C[Есть статус-код]
C --> D[Дальше разбираем статус и контракт]
Обратите внимание: мы сознательно не идём дальше в “разбираем контракт” — это как раз тема следующей лекции и итоговой схемы дня. Здесь наша задача ровно одна: научиться не путать «ошибочный ответ» и «отсутствие ответа».
Можно даже закрепить это маленькой утилитарной функцией (опять же, учебной):
class CallClassifier {
String classify(HttpSnapshot response, Exception error) {
// Сначала классифицируем "ответа нет": это отдельная ветка, без статуса и без body.
if (error != null) return "NETWORK_FAILURE";
// Если ответ есть, то можно безопасно смотреть на статус.
return "HTTP_RESPONSE_" + response.statusCode();
}
}
Да, это выглядит примитивно — и это хорошо. До появления реального HTTP-клиента нам не нужны тонкости. Нам нужна дисциплина: прежде чем обсуждать 404 и “что не так с endpoint”, убедись, что ответ вообще был.
7. Проект ReadLater Starter и сетевые сбои
Если вы всё ещё думаете «ну у нас же учебный проект, у нас будет всё стабильно», то я вас понимаю: хочется верить в добрый мир, где каждый запрос отвечает за 30 миллисекунд. Но проект ReadLater Starter специально построен так, чтобы вы почувствовали реальность интеграций. В первой фазе мы будем клиентом к внешнему каталогу книг. А внешний сервис — это как погода: можно смотреть прогноз, можно взять зонт, но нельзя договориться с облаками.
На практике это означает, что наша будущая команда вида «найди книги по запросу» может оказаться в ситуации “не получили ответа”. И тогда нельзя «изобразить 404». Нужно честно сообщить пользователю что-то вроде: «Каталог сейчас недоступен, попробуйте позже» — и это будет корректнее, чем попытка выжать из воздуха статус-код. В учебных примерах (пока без настоящей сети) это можно представить так:
class UserMessageDemo {
void showNetworkProblem() {
// В реальном приложении это мог бы быть UI/HTTP-ответ/сообщение в API,
// но для демонстрации нам достаточно консольного вывода.
System.out.println("Каталог книг недоступен. Попробуйте позже.");
// Каталог книг недоступен. Попробуйте позже.
}
}
Это мелочь, но она формирует привычку: пользовательскому сценарию нужен предсказуемый исход даже тогда, когда сеть решила поиграть с вами в «угадай, ответит ли сервис». Позже мы уже будем делать это на реальных вызовах и с нормальным логированием, но mental model строится сейчас: сеть может не ответить, и это не “статус 500”.
8. Типичные ошибки при сетевых сбоях
Когда начинаешь работать с удалёнными API, очень легко “перенести” на сеть привычки из локального кода: мол, раз я вызвал метод, то результат обязан вернуться. Увы, сеть не подписывала этот контракт. Поэтому ошибки здесь обычно не синтаксические, а мыслительные: неправильная интерпретация ситуации приводит к неправильным решениям дальше. Ниже — самые частые грабли, на которые наступают почти все.
Ошибка №1: считать, что раз что-то сломалось, значит «сервер вернул 500».
500 — это ответ, а не отсутствие ответа. Если сервис недоступен или соединение не установилось, у вас нет статус-кода. В голове важно держать фразу: «статус существует только если я получил HTTP-response». Иначе вы будете лечить не то: искать “почему 500”, когда нужно выяснять “почему ответа нет”.
Ошибка №2: путать 404 Not Found с недоступностью сервиса.
404 как раз говорит, что сервис доступен и обработал запрос — просто ресурс не найден. Это вообще другой класс проблемы, и клиент должен реагировать иначе. Если вы смешиваете эти случаи, вы будете показывать пользователю “сервис недоступен” там, где правильнее “книга не найдена”, и наоборот.
Ошибка №3: смотреть в body, не проверив факт наличия ответа.
Это классика: человек сразу лезет «читать тело ответа», а потом оказывается, что тела не было — был сбой соединения. В реальной реализации это проявляется как null, пустая строка, исключение на чтении потока или просто отсутствие данных. Правильный порядок мышления: ответ получен? → статус → headers/body.
Ошибка №4: относиться к медленному ответу как к «ну подождём ещё».
Если вы не вводите понятие лимита ожидания хотя бы на уровне мышления, ваш клиентский код будет зависать в неопределённости. Пользователь будет смотреть на «крутилку вечности», а ваш код — держать ресурсы в ожидании. Даже если сервер ответит через минуту, пользовательский сценарий уже сломан. Медленно — это не просто “неудобно”, это часто “неприемлемо”.
Ошибка №5: писать логику только для happy-path и удивляться, что «иногда всё падает».
Сетевые сбои не редкость, а норма. Если вы проектируете поведение приложения как “всегда получаем ответ”, вы получаете приложение, которое работает ровно до первого реального сбоя. Лучше заранее принять как данность две ветки: «ответ есть» и «ответа нет», и дать каждой из них понятный исход.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ