JavaRush /Курсы /Kotlin SELF /Ktor Client «без магии»: HttpClient, HttpResponse, bodyAs...

Ktor Client «без магии»: HttpClient, HttpResponse, bodyAsText()

Kotlin SELF
58 уровень , 1 лекция
Открыта

1. Введение

Когда вы впервые начинаете писать сетевой код, рука тянется к мечте: «хочу одну строчку, чтобы сразу пришёл ответ, сразу распарсился, сразу всё получилось, а если не получилось — ну… как-нибудь». Проблема в том, что сеть — чемпион по неожиданностям: сервер может ответить не тем, интернет может пропасть, API может вернуть HTML вместо JSON, а вы будете сидеть и думать, что это Kotlin «сломался».

Идея «без магии» означает: мы делаем все шаги явно. Мы явно получаем HttpResponse, явно смотрим статус, явно читаем тело (как текст), и только потом решаем, что делать дальше. Это делает код чуть длиннее, зато значительно проще отлаживать. И да: Ktor — это как раз один из популярных Kotlin‑инструментов для HTTP (и на клиенте, и на сервере). Он часто упоминается как Kotlin‑фреймворк для backend‑задач.

HttpClient: один объект, много запросов, одна ответственность

HttpClient в Ktor — это «главный пульт управления» сетевыми запросами. Его важнейшая идея: клиент создают один раз и переиспользуют. Если создавать новый клиент на каждый запрос, вы получите лишние подключения, лишние ресурсы и однажды — странные тормоза (или «почему оно зависает только на третьем запросе?»).

Ещё один ключевой момент: HttpClient надо закрывать. Он держит ресурсы (соединения, потоки), и если вы их не отпустили, программа в лучшем случае будет вести себя неаккуратно, а в худшем — вы увидите загадочные утечки и зависания. Поэтому мы с самого начала привыкаем к дисциплине: try/finally и client.close().

Для ориентира полезно держать в голове простую таблицу различий:

Сущность Что это Живёт сколько Зачем нужна
HttpClient
«машина», которая умеет делать HTTP обычно весь main/весь сценарий настраиваем «как ходим в сеть»
HttpResponse
конкретный ответ на конкретный запрос один запрос читаем статус, заголовки, тело

Движок и почему в примерах часто встречается CIO

На JVM Ktor Client может работать через разные «движки» (engine). Если говорить по‑человечески, engine — это «внутренняя реализация транспорта»: чем именно клиент будет реально устанавливать соединение и гонять байты. Для учебных примеров часто берут CIO, потому что он хорошо подходит как понятный дефолт для JVM.

Сейчас наша цель очень простая: научиться явно создать клиент, чтобы студент (то есть вы) не чувствовал, что половина работы сделана «по секрету».

Небольшая ремарка про зависимости: вам понадобятся модули ktor-client-core и engine для JVM (например, ktor-client-cio). Версии в реальном проекте подбираются под ваш Gradle/Kotlin, но как пример можно встретить запись вида io.ktor:ktor-client-core:2.3.11.

2. Минимальный клиент и первый запрос

Минимальная сборка клиента: HttpClient(CIO) без лишних слоёв

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

import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO

fun buildClient(): HttpClient {
    return HttpClient(CIO)
}

Обратите внимание: здесь ещё нет никаких плагинов, таймаутов и «умных» настроек. Мы намеренно держим уровень сложности минимальным: сначала научимся ходить, потом — бегать, потом — делать сальто (но не в проде, пожалуйста).

runBlocking в main: как подружить консоль и suspend

Ktor Client — корутинный. Это значит, что сетевые операции обычно делаются из suspend‑кода, потому что ожидание ответа не должно блокировать поток «в лоб». В консольном приложении у нас есть обычный fun main(), и он не suspend. Поэтому мы используем «мостик» — runBlocking { ... }, который вы уже видели в дне про корутины.

Важно воспринимать runBlocking как учебный/консольный инструмент: он запускает корутины и ждёт их завершения. Внутри этого блока мы можем спокойно вызывать client.get(...).

import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val client = HttpClient(CIO)
    println("Client created") // Client created
    client.close()
    println("Client closed")  // Client closed
}

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

Первый запрос «без магии»: получаем HttpResponse и читаем bodyAsText()

Теперь сделаем самый простой GET‑запрос. Принципиально важно: мы не пытаемся «сразу получить строку» или «сразу получить объект». Мы получаем HttpResponse, а затем отдельным шагом читаем тело.

Чтобы пример можно было реально запускать, часто используют тестовые сервисы вроде https://httpbin.org/get (он возвращает JSON, но мы пока читаем его как обычный текст).

import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val client = HttpClient(CIO)
    try {
        val response: HttpResponse = client.get("https://httpbin.org/get")
        val text: String = response.bodyAsText()

        println("status=${response.status.value}") // status=200 (если всё хорошо)
        println("body length=${text.length}")      // body length=...
    } finally {
        client.close()
    }
}

Обратите внимание на стиль: мы не прячем close() «куда-нибудь потом». Он стоит в finally, потому что сеть — место, где «потом» может не наступить (исключение, ошибка, отмена, что угодно).

Почему bodyAsText() — отдельный шаг, и почему это удобно

Иногда студент спрашивает: «А почему нельзя сделать так, чтобы get() сразу вернул строку?» Можно… но тогда вы теряете структуру HTTP‑ответа. А она важна. Статус‑код, заголовки и тело — это три разные части контракта, и смешивать их в одну кашу очень соблазнительно, но плохо для отладки.

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

Попробуем написать маленькую функцию‑помощник, которая «превращает ответ в понятные данные». Мы пока не лезем в JSON и модели (это будет отдельная тема), но уже можем оформить результат как data class.

import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText

data class HttpRawResponse(
    val status: Int,
    val body: String
)

suspend fun HttpResponse.toRaw(): HttpRawResponse {
    val text = bodyAsText()
    return HttpRawResponse(status = status.value, body = text)
}

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

3. Настройка запроса и небольшой рефакторинг

Request builder: где живут заголовки и query‑параметры

Почти любой реальный GET‑запрос рано или поздно превращается из «просто URL» в «URL + параметры + заголовки». В Ktor это настраивается через request builder — блок в фигурных скобках после get(...). Важно не путать роли: в этом блоке мы строим запрос, но не обрабатываем ответ.

Покажем пример, где мы добавляем Accept и query‑параметр q. Мы специально делаем это «по‑взрослому», без ручной склейки строки "?q=kotlin&limit=10" (потому что ручная склейка — это как писать пароли на стикерах: быстро, но потом стыдно).

import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.contentType

suspend fun demoSearch(client: HttpClient) {
    val response = client.get("https://httpbin.org/get") {
        header(HttpHeaders.Accept, ContentType.Application.Json)
        url { parameters.append("q", "kotlin") }
    }

    println("status=${response.status.value}")     // status=200
    println("body=${response.bodyAsText().take(40)}...") // body={ ... }...
}

Здесь мы уже видим красивую границу: «как строим запрос» — внутри { ... }, «что делаем с ответом» — после получения response.

Маленький рефакторинг: выносим сетевой вызов в функцию

Как только у вас появляется второй запрос, main начинает распухать. Это нормальный этап взросления программы: сначала всё в одном месте, потом вы начинаете выделять функции. Сейчас мы сделаем очень маленький, но полезный шаг: создадим функцию, которая делает GET и возвращает HttpRawResponse.

Обратите внимание: функция suspend, потому что внутри сеть. А main остаётся «дирижёром»: он создаёт клиент, вызывает функции и печатает результат.

import io.ktor.client.HttpClient
import io.ktor.client.request.get

suspend fun getText(client: HttpClient, url: String): HttpRawResponse {
    val response = client.get(url)
    return response.toRaw()
}

И использование:

import io.ktor.client.engine.cio.CIO
import io.ktor.client.HttpClient
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val client = HttpClient(CIO)
    try {
        val r = getText(client, "https://httpbin.org/get")
        println("status=${r.status}")           // status=200
        println("body length=${r.body.length}") // body length=...
    } finally {
        client.close()
    }
}

Этот рефакторинг кажется маленьким, но он дисциплинирует архитектуру: сетевой код перестаёт быть «просто кусочком в main» и становится отдельной единицей, которую проще читать, тестировать и развивать.

Небольшая схема: что происходит при «простой загрузке текста»

Чтобы закрепить модель в голове, полезно представить выполнение запроса как последовательность шагов. Да, это выглядит как «очевидно», но именно такие очевидные схемы спасают, когда что-то пошло не так, а вы смотрите в код в два часа ночи.

sequenceDiagram
    participant M as main/runBlocking
    participant C as HttpClient
    participant S as Server (URL)
    participant R as HttpResponse

    M->>C: get(url)
    C->>S: HTTP request (GET)
    S-->>C: HTTP response (status + headers + body)
    C-->>R: HttpResponse
    M->>R: status
    M->>R: bodyAsText()
    R-->>M: String (body text)

Ключевая мысль: HttpResponse — это не «текст ответа», это объект, из которого текст (или другие формы тела) извлекаются отдельным шагом.

4. Типичные ошибки при работе с HttpClient

Ошибка №1: создавать HttpClient внутри каждой функции запроса.
Новичок часто пишет fun load() { val client = HttpClient(...); client.get(...); } и повторяет это в пяти местах. Сначала кажется «норм», но потом появляются странные задержки, лишние подключения и непонятно где закрывать ресурсы. Правильнее держать один клиент выше по уровню и передавать его в функции.

Ошибка №2: забывать закрывать HttpClient.
Если клиент не закрывается, ресурсы могут висеть до завершения процесса, а иногда программа будет вести себя так, будто «не заканчивается» (особенно если вы дальше добавите фоновые операции). Привыкайте к try/finally { client.close() } как к чистке зубов: скучно, но полезно.

Ошибка №3: пытаться «читать тело» и «проверять статус» в одном неразборчивом комке.
Когда код превращается в простыню, вы начинаете ловить ошибки логики: например, парсите тело как будто это успех, хотя пришёл 404 или 500. Даже если вы пока просто печатаете текст, держите шаги раздельно: сначала response.status, потом bodyAsText(), потом решение.

Ошибка №4: склеивать URL с query‑параметрами вручную.
Строки вида "https://site/api?q=" + query + "&limit=" + limit очень быстро приводят к багам с ?/&, пробелами и экранированием. Ktor даёт url { parameters.append(...) } — используйте его. Это не «красота ради красоты», а профилактика «почему сервер не понимает мой запрос».

Ошибка №5: читать тело несколько раз и удивляться странностям.
Тело ответа — это поток данных, и в разных реализациях повторное чтение может быть проблемным или дорогим. Практический стиль простой: один раз вызвали bodyAsText(), сохранили в переменную и дальше работайте с этой строкой. Это и быстрее, и понятнее при отладке.

Ошибка №6: пытаться сделать сетевой вызов без корутин и удивляться, что компилятор ругается.
client.get(...)suspend‑операция, и Kotlin честно не даст вам вызвать её из обычной функции. Это не «Kotlin вредничает», это защита от блокировок и хаотичного асинхронного кода. Решение предсказуемое: либо вы внутри runBlocking (в консоли), либо вы в suspend‑функции.

1
Задача
Kotlin SELF, 58 уровень, 1 лекция
Недоступна
Жизнь клиента
Жизнь клиента
1
Задача
Kotlin SELF, 58 уровень, 1 лекция
Недоступна
Первый GET
Первый GET
1
Задача
Kotlin SELF, 58 уровень, 1 лекция
Недоступна
Сырой ответ
Сырой ответ
1
Задача
Kotlin SELF, 58 уровень, 1 лекция
Недоступна
Сборщик запроса
Сборщик запроса
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ