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 | |
какой протокол (обычно или ) |
| host | |
какой сервер (домен) |
| port | |
какой порт (часто скрыт, для https обычно 443) |
| path | |
какой ресурс/endpoint |
| query | |
параметры запроса «в строке» |
| 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.
Таблица для закрепления
| Метод | Типичный смысл | Есть ли тело запроса |
|---|---|---|
|
получить данные | обычно нет |
|
отправить данные / создать | обычно да |
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 — ошибка на стороне сервера (вы, может, сделали всё правильно, но сервер не смог обработать запрос).
Таблица: несколько кодов, которые встречаются постоянно
| Код | Название | Типичный смысл |
|---|---|---|
|
OK | всё хорошо, есть результат |
|
Created | что-то создано (часто после POST) |
|
Bad Request | запрос неправильный (формат/валидация) |
|
Unauthorized | не авторизован (обычно про токены/логин) |
|
Forbidden | доступ запрещён |
|
Not Found | ресурс не найден |
|
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, заголовки, ожидаемые статусы, формат тела) даёт вам стабильность. Он делает интеграцию повторяемой: другой разработчик сможет воспроизвести запрос, вы сможете написать понятную обработку ошибок, и ваше приложение будет вести себя одинаково в разные дни недели (даже в понедельник).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ