1. Вступ
Коли ви вперше чуєте про «мережу» і HTTP, може здаватися, ніби десь у тумані літають запити, сервери щось «віддають», а браузер «якось» показує результат. Насправді все набагато прозаїчніше (і від цього навіть приємніше): HTTP — це угода про те, як саме два учасники обмінюються повідомленнями. Не «як під’єднатися до Wi‑Fi» і не «як прокласти кабель через океан», а саме: який текст/байти ми надсилаємо й що вважаємо коректною відповіддю.
Слово «контракт» тут дуже влучне. Контракт — це коли ви не намагаєтеся вгадувати настрій сервера, а дієте за правилами: «я надсилаю ось це, у такому вигляді, з такими параметрами; ти відповідаєш ось так — із таким статусом і тілом». Якщо ви вмієте читати цей контракт, мережевий код перетворюється із шаманства на звичайне інженерне завдання: «зібрав запит → надіслав → розібрав відповідь».
Мініаналогія
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 | |
який порт (часто прихований; для зазвичай 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. Тепер ми хочемо уявити, що в нас зʼявився зовнішній сервіс, який зберігає витрати в хмарі.
Ми поки не пишемо мережевий код. Наша мета — навчитися читати й проєктувати контракт: які endpoint є, які методи, які статуси, які тіла.
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, заголовки, очікувані статуси, формат тіла) дає вам стабільність. Він робить інтеграцію відтворюваною: інший розробник зможе повторити запит, ви зможете написати зрозумілу обробку помилок, і ваш застосунок поводитиметься однаково в різні дні тижня (навіть у понеділок).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ