JavaRush /Курсы /Java Server /Сетевые сбои: нет ответа и таймауты

Сетевые сбои: нет ответа и таймауты

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

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 и удивляться, что «иногда всё падает».
Сетевые сбои не редкость, а норма. Если вы проектируете поведение приложения как “всегда получаем ответ”, вы получаете приложение, которое работает ровно до первого реального сбоя. Лучше заранее принять как данность две ветки: «ответ есть» и «ответа нет», и дать каждой из них понятный исход.

1
Задача
Java Server, 9 уровень, 2 лекция
Недоступна
Нет ответа или ответ с ошибкой
Нет ответа или ответ с ошибкой
1
Задача
Java Server, 9 уровень, 2 лекция
Недоступна
Лимит ожидания ответа
Лимит ожидания ответа
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ