JavaRush /Курсы /Java Server /HTTP-таймауты: connectTimeo...

HTTP-таймауты: connectTimeout и timeout

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

1. Таймауты как часть корректности

Удалённый HTTP-вызов — это не «быстрый метод, который иногда возвращает JSON». Это поход в реальный мир, где бывают пробки, закрытые двери и загадочные таблички «перерыв 15 минут» без указания, когда именно началось. Таймауты нужны не ради скорости, а ради предсказуемости: чтобы ваше приложение умело честно сказать «я подождал достаточно, дальше бессмысленно».

Если таймаутов нет, вы отдаёте управление времени операционной системе и сети. Иногда всё будет нормально. Иногда вызов будет висеть очень долго, потому что где-то по дороге пакет потерялся, роутер задумался о жизни, а ваш поток в это время просто стоит и ждёт. В консольном приложении это выглядит как «программа зависла». В backend-мире это ещё хуже: вы «занимаете» поток и не можете обслуживать другие задачи.

В нашем ReadLater Starter сейчас есть команды вроде catalog search .... Пользователь запускает команду и ожидает, что либо получит результат, либо быстро и понятно узнает, что каталог недоступен. Таймауты — это как раз про «быстро и понятно».

К этому месту у нас уже есть две базовые вещи: корректный URI и понятный HttpRequest. Но send() всё равно живёт по правилам сети, а не по правилам обычного Java-метода. Следующий вопрос transport-слоя очень приземлённый: сколько мы готовы ждать и что делать, если HttpResponse вообще не появился.

2. Соединение и ответ: два таймаута

Когда мы говорим «сервис не отвечает», это звучит как одна проблема, но технически там минимум два разных ожидания. Сначала клиент должен подключиться к удалённому хосту (это отдельная стадия), а потом — получить HTTP-ответ (это другая стадия). И таймауты у этих стадий разные, потому что причины «зависания» разные.

Схематично жизненный цикл синхронного запроса выглядит так:

flowchart TD
    A[Есть URI и HttpRequest] --> B[Пытаемся установить соединение]
    B -->|успех| C[Отправляем запрос]
    C --> D[Ждём ответ]
    D -->|успех| E[Получили HttpResponse]
    B -->|слишком долго| X["connect timeout"]
    D -->|слишком долго| Y["request timeout"]

Теперь закрепим различие в виде небольшой таблицы (она будет вашей «шпаргалкой на стену»):

Что ограничиваем по времени Где задаём Чем измеряем Что обычно происходит при превышении
Установление соединения (подключиться к удалённому хосту)
HttpClient.newBuilder()
          .connectTimeout(...)
Duration выбрасывается исключение из семейства HttpTimeoutException (часто HttpConnectTimeoutException)
Ожидание ответа на конкретный запрос
HttpRequest.newBuilder(...)
          .timeout(...)
Duration выбрасывается HttpTimeoutException (это тоже IOException)

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

3. connectTimeout: время подключения

connectTimeout мы задаём на уровне HttpClient. Логика простая: раз клиент — это «движок», который ходит по сети, то правило “сколько ждать подключения” обычно общее и повторяемое. Сегодня мы не строим сложную систему конфигурации (это будет потом по курсу), поэтому возьмём разумное учебное значение и сделаем его явным прямо в коде.

Минимальный пример (обратите внимание: здесь нет запроса — это только настройка клиента):

import java.net.http.HttpClient;
import java.time.Duration;

// Настраиваем "движок", который будет ходить по сети
HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(2)) // Сколько максимум ждём установления соединения
        .build(); // Клиент обычно создают один раз и переиспользуют

В этом месте у новичков часто возникает вопрос: «А 2 секунды — это нормально?» Ответ: для учебного проекта — более чем. В реальном мире значения зависят от окружения и требований, но сама привычка важнее цифры. Если поставить connectTimeout на 0.1 секунды, вы получите много ложных отказов. Если поставить на минуту — программа будет “зависать” слишком долго. Две-три секунды обычно дают хорошее ощущение “мы попробовали, но не вечность”.

Ещё один маленький нюанс, который полезно знать: HttpClient рассчитан на переиспользование. То есть «создать один раз и потом использовать для запросов» — нормальная стратегия. Если вы создаёте новый HttpClient на каждый запрос, вы не “сломаете интернет”, но добавите себе лишний шум в код и усложните контроль настроек.

4. HttpRequest.timeout(...): ожидание ответа

Таймаут запроса (HttpRequest.Builder.timeout(...)) задаётся для конкретного запроса. Это удобно: поиск может быть “быстрым”, а получение деталей — чуть “терпеливее”, или наоборот. Нам сейчас важна сама механика и то, как она меняет поведение client.send(...).

Пример GET с таймаутом на уровне запроса:

import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Duration;

URI uri = URI.create("https://catalog.example/search?q=java"); // Адрес удалённого каталога

HttpRequest request = HttpRequest.newBuilder(uri)
        .header("Accept", "application/json") // Просим JSON в ответе
        .timeout(Duration.ofSeconds(3)) // Сколько максимум ждём ответ именно на этот запрос
        .GET() // HTTP-метод
        .build();

Тут хорошая “читаемость сверху вниз”: сначала адрес, потом ожидания по формату (Accept), потом ограничение по времени (timeout), потом метод. Builder позволяет сделать так, что запрос выглядит как мини-документ: “что я хочу и на каких условиях”.

Если вы не зададите timeout(...) вообще, запрос может ждать очень долго. Да, иногда всё быстро. Но при сетевых проблемах вы получите “вечное ожидание”. Поэтому, если вы хотите взрослый клиент, таймауты — не опция, а дефолт.

5. Сбои client.send(...): 3 сценария

Самое важное здесь — перестать думать “ошибка = статус 500”. Для Java HTTP-клиента есть ситуации, когда никакого HttpResponse не будет, потому что запрос не завершился на сетевом уровне. И тогда вы получаете исключение. У client.send(...) в синхронной форме есть три «базовых вида неприятностей»: timeout, сетевой IOException и InterruptedException.

Посмотрим на минимальный, но честный шаблон обработки:

import java.io.IOException;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;

try {
    // Синхронный вызов: текущий поток ждёт завершения запроса или исключения
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println("Status = " + response.statusCode()); // например: Status = 200
} catch (HttpTimeoutException e) {
    // Таймаут — частный случай IOException, поэтому ловим его отдельно и раньше
    System.out.println("Каталог отвечает слишком долго"); // пример сообщения пользователю
} catch (IOException e) {
    // Сюда попадают прочие сетевые проблемы (DNS, TLS, разрыв соединения и т.д.)
    System.out.println("Сетевая ошибка: " + e.getClass().getSimpleName());
} catch (InterruptedException e) {
    // Поток попросили остановиться: корректно восстанавливаем флаг прерывания
    Thread.currentThread().interrupt();
    System.out.println("Запрос был прерван");
}

Обратите внимание на порядок catch. HttpTimeoutException — это частный случай IOException. Если вы первым поймаете IOException, то до HttpTimeoutException код никогда не дойдёт, и вы потеряете возможность дать пользователю более конкретное сообщение.

Теперь про InterruptedException. Это не «какая-то ещё ошибка сети». Это сигнал: “текущий поток попросили остановиться”. В учебном CLI это случается редко, но правильно реагировать нужно всегда. Минимально корректная реакция — восстановить флаг прерывания через Thread.currentThread().interrupt(); и прекратить нормальную обработку. Если вы проглотите прерывание и продолжите работать, можно получить очень странное поведение дальше (и, что хуже, это будет выглядеть как “рандом”).

6. IOException: сохраняем смысл

IOException — это большой зонт, под которым прячется много разных причин: не нашли хост (UnknownHostException), соединение отказано (часто когда неправильный порт), проблемы с сетью, проблемы TLS и так далее. На этом этапе курса нам не нужно устраивать экскурсию по всем видам сетевых исключений, но важно одно: не превращайте IOException в “ну, что-то сломалось”.

Даже простое сообщение, которое выводит тип исключения, уже даёт вам диагностическую опору:

} catch (IOException e) {
    // Общий случай: DNS, отказ в соединении, TLS и т.д.
    System.out.println("Сетевая ошибка при вызове каталога: " + e.getClass().getSimpleName());
    System.out.println("Details: " + e.getMessage()); // Текст зависит от конкретной причины
}

Заметьте, это не “логирование” в смысле production-логов — у нас пока просто консольный режим, и мы честно показываем пользователю/разработчику, что произошло. Позже (по траектории курса) вы перейдёте к нормальному logging stack, но привычка сохранять смысл ошибки нужна уже сейчас.

Ещё один важный принцип: не делайте catch (Exception e). Он кажется удобным, но он стирает грань между timeout, сетевой ошибкой и прерыванием. А мы как раз учимся видеть различия. HTTP-клиенту нужен не “универсальный ловец”, а осознанная развилка.

7. Таймауты без «удачи»

Иногда новички пытаются “поймать таймаут” на настоящем API и удивляются: “а он не ловится, всё быстро”. И это нормальная проблема: хороший сервис обычно отвечает быстро, а нам для обучения нужно увидеть ветку с ошибкой. Поэтому для демонстрации таймаута проще всего сделать хитрый (и немного жестокий) трюк: поставить слишком маленький request timeout и посмотреть, как клиент себя ведёт.

Например, вот такой запрос почти гарантированно не успеет:

import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Duration;

HttpRequest request = HttpRequest.newBuilder(URI.create("https://catalog.example/search?q=java"))
        .header("Accept", "application/json") // Просим JSON в ответе
        .timeout(Duration.ofMillis(1)) // Демонстрационный «почти гарантированный» таймаут
        .GET() // HTTP-метод
        .build();

При вызове client.send(...) вы с большой вероятностью получите HttpTimeoutException, и сработает ваш catch, который выводит “Каталог отвечает слишком долго”. Это искусственно, зато очень наглядно: вы видите, что ветка timeout действительно работает и не путается с IOException.

С connectTimeout похожая история: иногда при неправильном адресе вы получите не timeout, а, например, UnknownHostException (домен не найден) или “connection refused” (порт закрыт). И это тоже полезно, потому что показывает: ошибка “сервис недоступен” может выглядеть по-разному, и ваш код должен быть готов к этому. Наша цель — не угадать один конкретный текст ошибки, а научиться различать категории проблем.

8. Типичные ошибки при работе с таймаутами

Сетевой код — это место, где почти каждый новичок хотя бы раз наступает на одни и те же грабли. Это нормально: сеть просто очень не похожа на обычный вызов методов. Но эти ошибки лучше увидеть сейчас на маленьком учебном клиенте, чем потом в большом проекте (где они обычно встречаются в 3 часа ночи, когда вы просто хотели поспать).

Ошибка №1: «таймауты не нужны, у меня же всё локально».
Даже если ваш сервис “обычно отвечает быстро”, это не гарантия. Любая сеть иногда подвисает. Любой провайдер иногда деградирует. Если вы не задаёте таймауты, ваша программа не выбирает “как долго ждать” — за неё выбирает ОС и обстоятельства. В результате пользователь видит зависание и не понимает, что происходит.

Ошибка №2: ловить IOException раньше HttpTimeoutException и удивляться, что “timeout не работает”.
HttpTimeoutException — это IOException. Поэтому порядок catch важен. Если вы хотите отдельную ветку для таймаутов (а вы почти всегда хотите), ловите HttpTimeoutException первой.

Ошибка №3: проглатывать InterruptedException и продолжать выполнение.
Прерывание — это не “неприятность”, это протокол остановки. Минимально корректно: Thread.currentThread().interrupt(); и прекращение обработки. Если вы этого не делаете, можно получить странные проблемы в дальнейшем коде, потому что поток “просили остановиться”, а вы сделали вид, что не слышали.

Ошибка №4: ставить экстремально маленькие таймауты “чтобы было быстро”.
Таймаут — это не ускоритель интернета, а ограничитель ожидания. Слишком маленький таймаут превращает ваш клиент в капризного посетителя: “я постоял у двери 50 миллисекунд и ушёл”. В реальном мире это будет порождать ложные ошибки. Ставьте значения так, чтобы они выглядели как разумное ожидание для человека/системы, а не как попытка победить физику.

Ошибка №5: ловить всё через catch(Exception e) и выводить “Ошибка”.
Такой код кажется “надёжным”, но он убивает смысл. Вы перестаёте отличать timeout от сетевой ошибки, и особенно — от прерывания. В результате приложение становится непредсказуемым в поведении, а диагностика превращается в гадание по кофейной гуще.

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