JavaRush /Курсы /Kotlin SELF /Знакомство с HTTP

Знакомство с HTTP

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

1. Введение

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

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

Мини‑аналогия

HTTP можно воспринимать как заказ в кафе: вы говорите что хотите (метод), какой именно пункт меню (URL/endpoint), какие у вас пожелания (заголовки), иногда детали заказа (тело). Официант приносит вам чек со статусом (успех или ошибка) и, возможно, с блюдом (тело ответа). Если вы вместо «капучино» скажете «воду, но чтобы как кофе», получится странно — примерно так же странно серверу, когда вы путаете формат и смысл запроса.

2. Request и response: основная пара HTTP

Если вы вынесете из лекции одну мысль, пусть это будет она: HTTP — это всегда пара “request → response”. Вы отправляете запрос (request), а сервер возвращает ответ (response). Даже если ответ — это «ошибка», это всё равно ответ. Это принципиально: ошибка в HTTP часто выражается не исключением на вашей стороне, а нормальным ответом со статусом, который говорит «я тебя понял, но…».

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

flowchart LR
    C["Клиент (ваша программа)"] -->|HTTP request| S["Сервер (API)"]
    S -->|HTTP response| C

Как это выглядит «в сыром виде»

HTTP-сообщение — это заголовочная часть (строки) + иногда тело. Пугаться не надо: вам не нужно руками писать это всегда, но понимать структуру — обязательно.

Пример request (условный):

GET /api/expenses?limit=3 HTTP/1.1
Host: example.com
Accept: application/json

Пример response (условный):

HTTP/1.1 200 OK
Content-Type: application/json

[{"id":1,"title":"Coffee","amount":3.5}]

Заметьте важную вещь: в запросе есть «что хочу» (GET) и «куда» (/api/expenses?...). В ответе есть «чем закончилось» (200 OK) и «что получилось» (тело).

3. URL и endpoint: куда идём и что просим

URL часто путают с «ссылкой», но для клиента URL — это адрес ресурса. А endpoint — это обычно «конкретный путь API», то есть часть URL, которая описывает, какую именно функцию/ресурс вы вызываете на сервере. В разговоре разработчиков endpoint — это не философия, а сокращение: «какой путь дергаем».

Чтобы уверенно читать URL, полезно видеть его как набор частей. Например:

https://api.example.com:443/v1/users/42?verbose=true#fragment
Часть Пример Зачем нужна
scheme
https
какой протокол (обычно
http
или
https
)
host
api.example.com
какой сервер (домен)
port
443
какой порт (часто скрыт, для https обычно 443)
path
/v1/users/42
какой ресурс/endpoint
query
verbose=true
параметры запроса «в строке»
fragment
#fragment
для HTTP‑API обычно не используется (чаще про браузер)

Kotlin‑мини‑пример: разложим URL на части

Мы пока не ходим в сеть, просто тренируем мозг и строки.

fun main() {
    val url = "https://api.example.com/v1/users/42?verbose=true"
    val scheme = url.substringBefore("://")
    val rest = url.substringAfter("://")

    println("scheme=$scheme") // scheme=https
    println("rest=$rest")     // rest=api.example.com/v1/users/42?verbose=true
}

Да, это «разбор на коленке». Но он показывает важную мысль: URL — это строка со структурой, и позже инструменты будут делать то же самое, только безопаснее и удобнее.

4. HTTP‑метод: что именно мы делаем

Когда вы видите GET или POST, это не «просто слова», а часть контракта: метод сообщает серверу намерение. Для клиентской стороны сегодня нам достаточно двух методов, потому что они закрывают львиную долю реальных сценариев.

GET означает: «дай мне представление ресурса». Обычно это чтение данных. По-хорошему GET не должен менять состояние сервера (то есть быть «безопасным»), и обычно в GET нет тела запроса.

POST означает: «прими данные» или «создай что-то на основе этих данных». Часто POST используется, чтобы создать новую запись, запустить операцию, отправить форму. POST обычно идёт с телом, и очень часто это JSON.

Таблица для закрепления

Метод Типичный смысл Есть ли тело запроса
GET
получить данные обычно нет
POST
отправить данные / создать обычно да

Kotlin‑мини‑пример: метод + URL как осмысленная пара

Пока без сети, просто фиксируем идею: запрос начинается с «что делаем» + «куда».

fun main() {
    val method = "GET"
    val url = "https://api.example.com/v1/expenses?limit=10"

    println("$method $url") // GET https://api.example.com/v1/expenses?limit=10
}

5. Форматы запроса: заголовки и тело

Метод и URL отвечают на вопросы «что» и «куда». А дальше начинается практическая часть контракта: в каком виде мы общаемся, и какие именно данные передаём.

Заголовки (headers)

Заголовки в HTTP — это дополнительные поля «ключ: значение», которые помогают сторонам договориться о деталях. Если метод и URL отвечают на вопросы «что» и «куда», то заголовки отвечают на вопросы «в каком виде», «какие ограничения», «какие параметры передачи».

Очень важно не воспринимать заголовки как «какую-то скучную служебную часть». На практике в API заголовки часто решают всё: формат данных, язык, версию, авторизацию (но авторизацию мы сегодня сознательно не берём глубоко).

Два заголовка, которые вам стоит запомнить уже сейчас:

Accept — «какой формат ответа я хочу получить». Если вы ожидаете JSON, логично сказать об этом.

Content-Type — «какой формат у моего тела запроса». Это важно, когда вы отправляете данные (например, JSON в POST). Если вы не сказали Content-Type, сервер может интерпретировать тело неправильно или отказаться.

Kotlin‑мини‑пример: представим заголовки как Map

Для понимания структуры очень удобно думать о заголовках как о словаре.

fun main() {
    val headers = mapOf(
        "Accept" to "application/json",
        "Content-Type" to "application/json"
    )

    println(headers["Accept"])       // application/json
    println(headers["Content-Type"]) // application/json
}

Да, в реальном HTTP заголовки — это строки, регистр ключей не всегда важен, и там есть нюансы. Но как модель для новичка Map<String, String> — отличная стартовая точка.

Тело (body)

Тело запроса или ответа — это «полезная нагрузка». Если вы делаете GET, чаще всего вы ничего не отправляете в теле, а получаете тело в ответе. Если вы делаете POST, часто вы отправляете тело (например, JSON), и в ответ тоже может прийти тело (например, созданный объект или сообщение об ошибке).

Ключевой момент: тело — это не обязательно строка. Технически это набор байтов. Но в нашем курсе (и в большинстве типичных API‑сценариев) мы очень часто будем думать о теле как о тексте, чаще всего о JSON-тексте.

Kotlin‑мини‑пример: соберём JSON‑тело как строку

Мы пока не сериализуем модель, а просто показываем идею «в теле лежит JSON».

fun main() {
    val title = "Coffee"
    val amount = 3.5

    val jsonBody = """{"title":"$title","amount":$amount}"""
    println(jsonBody) // {"title":"Coffee","amount":3.5}
}

Позже вы будете делать это через kotlinx.serialization, и это будет надёжнее (не надо вручную следить за кавычками), но идея останется той же: тело — это данные по договорённому формату.

6. Статус‑код: как сервер сообщает результат

Статус‑код — это первое, на что вы должны смотреть в ответе. Он отвечает на вопрос: «чем закончилась попытка». И именно слово попытка тут важное. Сервер мог быть недоволен вашим запросом, мог упасть сам, мог успешно обработать всё — и всё это выражается числами.

Вам не нужно зубрить все коды. Достаточно уверенно понимать классы:

2xx — успех (запрос обработан).

4xx — ошибка на стороне клиента (вы отправили не то, не туда, не в том формате, не с теми данными).

5xx — ошибка на стороне сервера (вы, может, сделали всё правильно, но сервер не смог обработать запрос).

Таблица: несколько кодов, которые встречаются постоянно

Код Название Типичный смысл
200
OK всё хорошо, есть результат
201
Created что-то создано (часто после POST)
400
Bad Request запрос неправильный (формат/валидация)
401
Unauthorized не авторизован (обычно про токены/логин)
403
Forbidden доступ запрещён
404
Not Found ресурс не найден
500
Internal Server Error ошибка внутри сервера

Kotlin‑мини‑пример: проверка «успех ли это»

Очень полезно сразу привыкнуть к функции «успех — это диапазон».

fun isSuccessful(status: Int): Boolean = status in 200..299

fun main() {
    println(isSuccessful(200)) // true
    println(isSuccessful(404)) // false
}

Психологически это важнее, чем «проверять на 200». Потому что успешных статусов несколько, и ваш код должен быть к этому готов (201, 204 и т.д.).

7. Порядок действий: статус → ветка → интерпретация тела

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

Сначала вы получаете ответ и читаете статус. Потом решаете, это успех или ошибка. И только после этого интерпретируете тело: либо как «данные», либо как «сообщение об ошибке» (или другой формат, в зависимости от API).

Эту дисциплину удобно держать в голове как маленькую блок‑схему:

flowchart TD
    A["Получили response"] --> B["Проверили status"]
    B -->|2xx| C["Читаем тело как данные (например, JSON)"]
    B -->|non-2xx| D["Читаем тело как ошибку/сообщение"]

Kotlin‑мини‑пример: результат запроса как тип

Мы не делаем реальный запрос, но моделируем грамотный контракт результата (вы это уже умеете по sealed class).

sealed class HttpCallResult {
    data class Ok(val body: String) : HttpCallResult()
    data class HttpError(val status: Int, val body: String) : HttpCallResult()
}

fun handle(status: Int, body: String): HttpCallResult =
    if (status in 200..299) HttpCallResult.Ok(body)
    else HttpCallResult.HttpError(status, body)

fun main() {
    val r = handle(404, """{"error":"not found"}""")
    println(r) // HttpError(status=404, body={"error":"not found"})
}

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

8. Контракт для учебного приложения ExpenseTracker

Чтобы сегодняшняя лекция была не только «про интернет в вакууме», давайте привяжем её к нашему условному практическому CLI‑приложению, которое ведёт учёт расходов (назовём его ExpenseTracker). До этого момента оно жило локально: ввод, коллекции, файлы, JSON. Теперь мы хотим представить, что у нас появился внешний сервис, который хранит расходы в облаке.

Мы пока не пишем сетевой код. Наша цель — научиться читать и проектировать контракт: какие endpoints есть, какие методы, какие статусы, какие тела.

Endpoint для получения списка расходов

Представим, что сервер предоставляет endpoint:

GET https://api.example.com/v1/expenses?limit=3

Тогда «сырое» взаимодействие можно вообразить так.

Request (пример):

GET /v1/expenses?limit=3 HTTP/1.1
Host: api.example.com
Accept: application/json

Response при успехе:

HTTP/1.1 200 OK
Content-Type: application/json

[
  {"id": 1, "title": "Coffee", "amount": 3.5},
  {"id": 2, "title": "Lunch",  "amount": 12.0}
]

И здесь контракт говорит нам сразу несколько вещей. Метод GET и отсутствие тела запроса намекают: это чтение. Accept: application/json говорит: «мы хотим JSON». 200 OK говорит: «успех». Content-Type в ответе подтверждает: «да, это JSON».

Endpoint для добавления расхода

Теперь добавление:

POST https://api.example.com/v1/expenses

Тело запроса — JSON с данными нового расхода.

Request (пример):

POST /v1/expenses HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{"title":"Coffee","amount":3.5}

Response при успехе (вариант):

HTTP/1.1 201 Created
Content-Type: application/json

{"id": 10, "title":"Coffee","amount":3.5}

Очень типичный паттерн: сервер вернул 201 Created и прислал созданный объект с назначенным id.

Kotlin‑мини‑пример: заготовка данных запроса как модель

Пусть у нас уже есть модель (без сериализации, просто структура):

data class NewExpense(
    val title: String,
    val amount: Double
)

fun main() {
    val expense = NewExpense(title = "Coffee", amount = 3.5)
    println(expense) // NewExpense(title=Coffee, amount=3.5)
}

Сегодня этого достаточно: мы фиксируем, что тело запроса — это представление наших данных, обычно в JSON.

9. Типичные ошибки при понимании HTTP‑контракта

Ошибка №1: считать, что «если пришло тело — значит успех».
Это очень человеческая ловушка: вы получили какой-то текст, увидели фигурные скобки, обрадовались и начали парсить как данные. Но сервер вполне может вернуть JSON‑ошибку со статусом 400 или 404. Поэтому дисциплина «сначала статус, потом интерпретация тела» экономит часы жизни и килограммы нервов (а нервы, как известно, не val, а скорее var, и их очень легко испортить).

Ошибка №2: путать смысл GET и POST.
Новички иногда используют POST «потому что так работает», или наоборот пытаются отправить тело в GET «потому что мне так удобнее». На практике API почти всегда ожидают стандартную семантику: GET — читать, POST — отправлять/создавать. Когда вы держитесь этой семантики, ваш код становится предсказуемым для других разработчиков и для серверной стороны, а непредсказуемость лучше оставить котам и квантовой физике.

Ошибка №3: игнорировать форматы и заголовки.
Можно попытаться «просто отправить текст» и надеяться, что сервер догадается. Иногда он догадается. Иногда нет. А иногда он «догадается неправильно» — и это самый опасный вариант, потому что вы получите странные ошибки. Заголовки Accept и Content-Type — это ваш способ не гадать на кофейной гуще, а явно договориться о формате. Особенно важно помнить Content-Type, когда вы отправляете JSON‑тело.

Ошибка №4: ожидать, что HTTP‑ошибка — это обязательно исключение.
В Kotlin вы привыкли: что-то пошло не так — ловим Exception. В HTTP логика другая: сервер может честно вернуть 404 как обычный ответ, и это не «сбой сети», а результат обработки запроса. Это две разные категории проблем: «сервер ответил ошибкой» и «мы вообще не смогли выполнить запрос». Сегодня мы это только фиксируем на уровне смысла; в следующих лекциях вы уже научитесь разделять эти случаи в коде.

Ошибка №5: «забывать» про контракт и пытаться угадать поведение по опыту.
Иногда кажется, что проще попробовать «как-нибудь вызвать URL», а дальше «посмотрим». Но именно HTTP‑контракт (метод, URL, заголовки, ожидаемые статусы, формат тела) даёт вам стабильность. Он делает интеграцию повторяемой: другой разработчик сможет воспроизвести запрос, вы сможете написать понятную обработку ошибок, и ваше приложение будет вести себя одинаково в разные дни недели (даже в понедельник).

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