HTTP — протокол вебу
HTTP — протокол вебу
Від теорії до практики: чому HTTP існує
У попередніх розділах ми вивчили транспортний рівень — UDP та TCP. Обидва протоколи вирішують задачу доставки байтів між процесами. Але що робити з цими байтами? Який сенс несуть вони для застосунку? Відповідь на це питання дає прикладний рівень (Application Layer, Layer 7 моделі OSI).
Саме на прикладному рівні живе HTTP — HyperText Transfer Protocol. Він визначає не те, як байти доставляються (це справа TCP), а що вони означають: який ресурс запитується, яким методом, яка відповідь очікується і що вона містить.
HTTP є основою World Wide Web і одним з найважливіших протоколів в історії комп'ютерних мереж. Кожного разу, коли ви відкриваєте браузер, кожен REST API виклик, кожне завантаження файлу — все це HTTP.
Історія та еволюція стандарту
HTTP пройшов довгий шлях від однорядкового протоколу до складної багаторівневої специфікації. Розуміння цього шляху пояснює багато сучасних рішень і «дивних» деталей протоколу.
Зверніть увагу на ключове слово stateless — HTTP не зберігає стан між запитами. Кожен запит є незалежним, і сервер не пам'ятає попередніх взаємодій. Це свідоме архітектурне рішення, що забезпечує масштабованість, але й породжує потребу в механізмах на кшталт cookies та сесій (які ми розглянемо в наступному розділі).
Детальна хронологія версій
HTTP/0.9 — «Одного рядка достатньо» (1991)
Тім Бернерс-Лі у 1991 році у CERN (Женева) запропонував першу версію HTTP. Протокол настільки простий, що його навіть не нумерували — назва «0.9» з'явилась ретроспективно, коли вийшла версія 1.0.
Можливості: лише один метод (GET), лише HTML у відповіді, без заголовків, без статус-кодів, з'єднання закривається після кожного запиту.
GET /index.html
Відповідь — просто HTML без жодних метаданих:
<html>
<body>
<p>Hello, World!</p>
</body>
</html>
Якщо щось йде не так — з'єднання просто розривається. Жодного способу повідомити про помилку.
HTTP/1.0 — Заголовки і методи (RFC 1945, 1996)
Публікація RFC 1945 у 1996 році (фактична специфікація практики, що вже склалась) принесла революційні зміни:
- Заголовки (
Content-Type,Content-Length,Dateтощо) - Методи
POSTтаHEADна додачу доGET - Статус-коди (200, 404, 500...)
- Версія протоколу в запиті
GET /index.html HTTP/1.0
Accept: text/html
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 137
<html>...</html>
Критичне обмеження: кожен запит вимагав нового TCP-з'єднання. Сторінка з 30 зображеннями → 31 TCP-з'єднання (1 для HTML + 30 для зображень). Кожне з'єднання — це 3-way handshake + повільний старт TCP. При латентності 100мс — лише на рукостискання витрачалось 3 секунди.
HTTP/1.1 — Keep-Alive та віртуальний хостинг (RFC 2068/7230, 1997–2014)
HTTP/1.1 — найдовговічніша версія, що й досі широко використовується. Ключові нововведення:
Persistent connections (Keep-Alive): TCP-з'єднання залишається відкритим для багатьох запитів. Перший запит встановлює з'єднання, наступні йдуть по вже готовому каналу.
Обов'язковий заголовок Host: Один IP-сервер може обслуговувати тисячі доменів — саме Host header дозволяє серверу зрозуміти, для якого сайту запит.
GET /page HTTP/1.1
Host: www.example.com
Connection: keep-alive
Chunked Transfer Encoding: Сервер може починати надсилати відповідь до того, як знає повний розмір:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
1a
Перший шматок даних...
f
Другий шматок.
0
Pipelining (теоретично): Клієнт може надсилати кілька запитів не чекаючи відповіді. На практиці — майже ніколи не використовується через проблему Head-of-Line blocking: відповіді повинні йти в тому ж порядку, що й запити. Якщо перший запит повільний — всі наступні чекають.
HTTP/2 — Бінарний мультиплексинг (RFC 7540, 2015)
Розроблений Google як протокол SPDY. Повністю зворотньо сумісний з HTTP (ті самі методи, коди, заголовки) — але радикально інший на транспортному рівні.
Бінарний фреймінг: замість текстового протоколу — двійковий. Ефективніший для парсингу, менше помилок.
Мультиплексування: кілька запитів одночасно в одному TCP-з'єднанні. Кожен запит — окремий «потік» (stream). Немає HOL blocking на рівні застосунку.
HPACK: Стиснення заголовків зі статичними та динамічними таблицями. Замість повторної відправки Content-Type: application/json — лише індекс у таблиці.
Server Push: Сервер може «заздалегідь» надіслати ресурси, що знадобляться клієнту (наприклад, CSS та JS разом з HTML).
HTTP/3 — Прощай, TCP (RFC 9114, 2022)
Головна проблема HTTP/2: TCP HOL blocking. Якщо один TCP-пакет загубився — всі потоки HTTP/2 чекають його перепередачі, навіть не пов'язані між собою.
HTTP/3 замінює TCP на QUIC — протокол транспортного рівня поверх UDP, розроблений Google. QUIC будує власний механізм надійності на рівні потоків, тому втрата пакету одного потоку не блокує інші.
URL: Уніфікований локатор ресурсів
Що таке URL
URL (Uniform Resource Locator, RFC 3986) — це рядок, що однозначно ідентифікує ресурс у мережі та вказує спосіб отримання доступу до нього. Кожна частина URL несе чітко визначений сенс.
Анатомія URL
https://user:password@api.example.com:8443/v1/users/42?role=admin&page=2#profile
└──┬──┘ └──┬─────────┘ └──────┬──────┘ └┬─┘ └──────┬──────┘ └────────┬───────┘ └──┬──┘
Scheme UserInfo Host Port Path Query Fragment
Детальний розбір кожної компоненти
http— незахищений HTTP (порт 80 за замовчуванням)https— HTTP over TLS (порт 443 за замовчуванням)ftp— File Transfer Protocol (порт 21)ws/wss— WebSocket (незахищений / захищений)mailto— email-адреса (не HTTP)file— локальна файлова система
user:password@) — базова автентифікація в URL. Застаріла практика — паролі видно в логах та адресному рядку. Ніколи не використовуйте у production.api.example.com) або IPv4 (192.168.1.1) або IPv6 у дужках ([::1]). Регістронезалежний. Резолвиться через DNS у IP-адресу.https://example.com:443/ = https://example.com/ (443 — порт за замовчуванням для HTTPS)./. Компоненти розділяються /.Правила:%XX— URL-кодування небезпечних символів (пробіл →%20або+у query)..— піддиректорія вгору (сервери повинні нормалізувати!)- Регістрочутливий на Unix-серверах, регістронезалежний на Windows IIS
/users— колекція/users/42— конкретний ресурс за ID/users/42/orders— вкладений ресурс/users/42/orders/7/items— глибоко вкладений
?. Пари key=value розділяються &. Порядок не гарантований (але на практиці зберігається).Символи, що потребують URL-кодування: пробіл (%20), & (%26), = (%3D), + (%2B), # (%23), % (%25).Типові використання:?search=HTTP%20протокол — пошук
?page=2&limit=20 — пагінація
?sort=name&order=asc — сортування
?filter[role]=admin — фільтрація (нестандартний синтаксис)
?ids[]=1&ids[]=2&ids[]=3 — масив значень
?callback=myFunc — JSONP (застаріло)
#. Ніколи не надсилається серверу — обробляється виключно браузером. Використовується для:- Навігації до розділу сторінки (
#section-headers) - Single Page Application routing (
#/users/42) - OAuth redirect URI state (
#access_token=...)
URL vs URI vs URN
Ці терміни часто плутають:
| Термін | Розшифровка | Що ідентифікує | Приклад |
|---|---|---|---|
| URI | Uniform Resource Identifier | Будь-який ресурс | https://..., urn:isbn:... |
| URL | Uniform Resource Locator | Ресурс + де знаходиться | https://api.example.com/users |
| URN | Uniform Resource Name | Ресурс за іменем (без локації) | urn:isbn:978-3-16-148410-0 |
URL — підмножина URI. Будь-який URL є URI, але не кожен URI є URL.
Абсолютні та відносні URL
# Абсолютний URL — повний, самодостатній
https://api.example.com/v1/users/42
# Відносний від схеми (protocol-relative) — рідко, але буває
//api.example.com/v1/users
# Відносний від кореня хоста
/v1/users/42
# Відносний від поточного шляху
../orders → /v1/orders (якщо поточний /v1/users/42)
./profile → /v1/users/profile
URL-кодування у C#
using System.Net;
// ── Кодування компонент URL ──────────────────────────────────────────────────
string rawQuery = "Пошук HTTP протоколу & cookies";
// Uri.EscapeDataString — для кодування значення параметра (пробіл → %20)
string encoded = Uri.EscapeDataString(rawQuery);
// → "%D0%9F%D0%BE%D1%88%D1%83%D0%BA%20HTTP%20%D0%BF%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%D1%83%20%26%20cookies"
// Uri.EscapeUriString — для кодування повного URI (зберігає структурні символи)
string uri = Uri.EscapeUriString("https://example.com/шлях?ключ=значення");
// Декодування
string decoded = Uri.UnescapeDataString(encoded);
// ── Побудова URL з параметрами ───────────────────────────────────────────────
// Через UriBuilder — структурований підхід
var builder = new UriBuilder("https://api.example.com")
{
Path = "/v1/users",
Port = -1, // -1 = порт за замовчуванням (не додавати до URL)
};
// Query string — Dictionary + Uri.EscapeDataString (не потребує System.Web)
var queryParams = new Dictionary<string, string>
{
["page"] = "2",
["limit"] = "20",
["search"] = "HTTP протокол"
};
builder.Query = string.Join("&",
queryParams.Select(kvp =>
$"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}"));
Console.WriteLine(builder.Uri);
// https://api.example.com/v1/users?page=2&limit=20&search=HTTP%20%D0%BF%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB
// ── Короткий рядковий варіант ──────────────────────────────────────────────
string baseUrl = "https://api.example.com/v1/users";
string fullUrl = $"{baseUrl}?page=2&limit=20&search={Uri.EscapeDataString(\"HTTP протокол\")}";
Місце HTTP у стеці TCP/IP
Перш ніж розбирати деталі протоколу, критично важливо розуміти, де саме він знаходиться у стеці мережевих протоколів та на чому базується.
| Рівень | Протокол | Відповідає за |
|---|---|---|
| Прикладний | HTTP | Семантика запитів і відповідей |
| Транспортний | TCP | Надійна доставка, порядок, без втрат |
| Мережевий | IP | Маршрутизація між мережами |
| Канальний | Ethernet/Wi-Fi | Доставка в межах одного сегменту |
Анатомія HTTP-повідомлення
HTTP-повідомлення бувають двох типів: запит (Request) від клієнта до сервера та відповідь (Response) від сервера до клієнта. Обидва мають однакову загальну структуру, але різні перші рядки.
Структура HTTP-запиту
Кожен HTTP-запит складається з трьох частин: стартового рядка, заголовків та (опціонально) тіла.
У «сирому» вигляді (як передається по TCP) HTTP-запит виглядає так:
GET /api/users?page=1&limit=10 HTTP/1.1
Host: api.example.com
Accept: application/json
Accept-Language: uk-UA,uk;q=0.9
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
User-Agent: Mozilla/5.0 (compatible; MyApp/1.0)
Connection: keep-alive
<empty line \n\r>
Структура HTTP-відповіді
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 348
Cache-Control: max-age=3600, public
X-Request-Id: a3f8b2c1-d9e2-4f1a-b7c3-8e9d0a1f2b3c
Date: Sun, 17 May 2026 09:00:00 GMT
[
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"}
]
HTTP/1.0, HTTP/1.1 або HTTP/2. У HTTP/2 статусний рядок передається у бінарному форматі через псевдо-заголовок :status, але семантика залишається тією самою.1xx — інформаційні, 2xx — успішні, 3xx — редиректи, 4xx — помилки клієнта, 5xx — помилки сервера.OK, Not Found, Internal Server Error. У HTTP/2 цей рядок відсутній — лише числовий код. Застосунки не повинні покладатись на reason phrase у своїй логіці.Content-Type = content-type). Один заголовок може мати кілька значень, розділених комою: Accept: text/html, application/json.Content-Type. Розмір — заголовком Content-Length (або через Transfer-Encoding: chunked для потокової передачі невідомого розміру).HTTP в дії: повні сценарії запит–відповідь
Подивімося, як виглядають реальні HTTP-діалоги між клієнтом і сервером. Саме такий текст передається по TCP-з'єднанню — символ за символом, без жодних абстракцій.
telnet api.example.com 80 і вручну надрукувати HTTP-запит — і сервер відповість. Цей факт підкреслює простоту і прозорість протоколу.Сценарій 1: GET — отримання ресурсу
GET /api/users/42 HTTP/1.1
Host: api.example.com
Accept: application/json
Accept-Encoding: gzip, deflate, br
User-Agent: dotnet-httpclient/8.0
Connection: keep-alive
Зверніть увагу на порожній рядок наприкінці — він обов'язковий. Саме він сигналізує серверу, що всі заголовки передано.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 112
Cache-Control: max-age=60, public
ETag: "a3f8b2c1"
Date: Thu, 22 May 2026 10:00:00 GMT
{
"id": 42,
"name": "Олег Коваленко",
"email": "oleg@example.com",
"role": "developer"
}
Якщо клієнт додає If-None-Match і ресурс не змінився — сервер відповідає без тіла:
GET /api/users/42 HTTP/1.1
Host: api.example.com
If-None-Match: "a3f8b2c1"
HTTP/1.1 304 Not Modified
ETag: "a3f8b2c1"
Cache-Control: max-age=60, public
Date: Thu, 22 May 2026 10:05:00 GMT
Тіло відсутнє — клієнт використовує кешовану версію. Трафік зекономлено.
Сценарій 2: POST — створення ресурсу
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 100
Accept: application/json
{
"name": "Марія Шевченко",
"email": "maria@example.com",
"role": "designer"
}
Тіло йде після порожнього рядка. Content-Length вказує серверу, скільки байтів читати.
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 138
Location: /api/users/43
Date: Thu, 22 May 2026 10:01:00 GMT
{
"id": 43,
"name": "Марія Шевченко",
"email": "maria@example.com",
"role": "designer",
"created_at": "2026-05-22T10:01:00Z"
}
Location: /api/users/43 — URL нового ресурсу, обов'язковий при 201 Created.
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"name": "",
"email": "not-an-email"
}
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"status": 422,
"title": "Validation Error",
"errors": {
"name": ["Не може бути порожнім"],
"email": ["Невалідний формат email"]
}
}
Сценарій 3: DELETE та ідемпотентність
DELETE /api/users/43 HTTP/1.1
Host: api.example.com
HTTP/1.1 204 No Content
Date: Thu, 22 May 2026 10:02:00 GMT
204 No Content — успіх без тіла. Ресурс видалено.
DELETE /api/users/43 HTTP/1.1
Host: api.example.com
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"status": 404,
"title": "Not Found",
"detail": "Користувача з ID 43 не існує"
}
Ресурс вже видалено → 404. Стан системи не змінився — це і є ідемпотентність DELETE.
Сценарій 4: Редирект HTTP → HTTPS
GET /api/data HTTP/1.1
Host: example.com
HTTP/1.1 301 Moved Permanently
Location: https://example.com/api/data
Content-Length: 0
Сервер не обробляє запит — одразу перенаправляє на HTTPS.
GET /api/data HTTP/1.1
Host: example.com
Connection: keep-alive
HTTP/1.1 200 OK
Content-Type: application/json
Strict-Transport-Security: max-age=31536000; includeSubDomains
{"data": [...]}
Strict-Transport-Security гарантує, що всі майбутні запити одразу підуть через HTTPS.
На рівні HTTP/1.1 текст запиту виглядає так само, як звичайний HTTP — https у рядку запиту відсутній. Різниця на транспортному рівні: TCP-з'єднання встановлено через TLS на порту 443, і весь текст передається у зашифрованому вигляді.
Мінімальний vs. реальний запит
Браузери автоматично додають десятки заголовків. HttpClient у .NET — лише необхідний мінімум:
Мінімальний (HttpClient)
GET /api/users HTTP/1.1
Host: api.example.com
Accept: application/json
Лише те, що потрібно серверу для обробки.
Реальний (браузер Chrome)
GET /api/users HTTP/1.1
Host: api.example.com
Accept: text/html,application/xhtml+xml,*/*;q=0.8
Accept-Language: uk-UA,uk;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br, zstd
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
DNT: 1
Браузер додає узгодження формату, мови, кодування та заголовки безпеки.
HTTP-методи: повний академічний розбір
HTTP-метод визначає намір клієнта — що саме він хоче зробити з ресурсом. RFC 7231 визначає вісім стандартних методів, кожен з яких має чітку семантику та важливі властивості.
Класифікація методів
Два критично важливих поняття для розуміння методів:
Ідемпотентність (Idempotency)
Метод є ідемпотентним, якщо повторне виконання ідентичного запиту дає той самий ефект, що і одноразове виконання. Тобто: f(f(x)) = f(x).
Практичне значення: при мережевому збої клієнт може безпечно повторити ідемпотентний запит, не боячись подвійного ефекту.
Безпечність (Safety)
Метод є безпечним, якщо він не змінює стан сервера (read-only операція). Безпечні методи можна кешувати, і вони не мають побічних ефектів.
Важливо: всі безпечні методи є ідемпотентними, але не навпаки. DELETE — ідемпотентний, але не безпечний.
| Метод | Safe | Idempotent | Body | Типове використання |
|---|---|---|---|---|
GET | ✅ | ✅ | ❌ | Отримати список або ресурс |
HEAD | ✅ | ✅ | ❌ | Перевірити існування, metadata |
OPTIONS | ✅ | ✅ | ❌ | CORS preflight, можливості |
POST | ❌ | ❌ | ✅ | Створити ресурс, надіслати форму |
PUT | ❌ | ✅ | ✅ | Замінити ресурс повністю |
PATCH | ❌ | ❌* | ✅ | Часткове оновлення |
DELETE | ❌ | ✅ | ❌ | Видалити ресурс |
CONNECT | ❌ | ❌ | — | Проксі-тунель |
PATCH {"increment": 1} збільшить лічильник щоразу по-іншому. Але PATCH {"name": "Alice"} — фактично ідемпотентний. Ідемпотентність PATCH залежить від семантики операції, а не від самого методу.Детальний розбір кожного методу
GET запитує представлення ресурсу. Параметри передаються лише через URL (query string). Ніколи не передавайте чутливі дані через GET — URL логується у браузері, проксі та серверних access-логах.
Приклад 1: Отримання списку
GET /api/users HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGci...
HTTP/1.1 200 OK
Content-Type: application/json
X-Total-Count: 2
{
"users": [
{"id": 1, "name": "Іван"},
{"id": 2, "name": "Марія"}
]
}
Приклад 2: Отримання конкретного ресурсу
GET /api/users/1 HTTP/1.1
Host: api.example.com
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1,
"name": "Іван",
"email": "ivan@example.com"
}
Приклад 3: Фільтрація, сортування, пагінація
GET /api/products?category=electronics&sort=price&order=asc&page=2&limit=10 HTTP/1.1
Host: shop.example.com
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
X-Total-Count: 143
{
"data": [...],
"meta": {"total": 143, "page": 2, "per_page": 10}
}
Коли використовувати:
- Отримання одного або колекції ресурсів
- Пошук і фільтрація
- Пагінація
- Будь-яка read-only операція
POST надсилає дані для обробки. Найчастіше: створення нового ресурсу або виконання дії (login, upload, checkout).
Приклад 1: Створення ресурсу
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 62
{
"name": "Олена",
"email": "olena@example.com",
"role": "user"
}
HTTP/1.1 201 Created
Location: /api/users/43
Content-Type: application/json
{
"id": 43,
"name": "Олена",
"email": "olena@example.com",
"created_at": "2026-05-17T09:00:00Z"
}
Приклад 2: Логін (дія, що не створює REST-ресурс)
POST /api/auth/login HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"email": "ivan@example.com",
"password": "s3cr3t"
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"token": "eyJhbGci...",
"expires_in": 3600
}
Приклад 3: Відправка форми (HTML form)
POST /contact HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
name=Іван&email=ivan%40example.com&message=Привіт
HTTP/1.1 303 See Other
Location: /contact/thanks
Коли використовувати:
- Створення нового ресурсу (відповідь
201) - Виконання дії (
/orders/42/pay,/emails/send) - Відправка великих або чутливих даних
- Завантаження файлів
PUT повністю замінює ресурс за вказаним URL. Тіло запиту — це нове повне представлення. Поля, не вказані у тілі PUT, втрачаються.
Приклад 1: Оновлення (повна заміна)
PUT /api/users/1 HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"id": 1,
"name": "Іван Петренко",
"email": "ivan.new@example.com",
"role": "admin"
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1,
"name": "Іван Петренко",
"email": "ivan.new@example.com",
"role": "admin",
"updated_at": "2026-05-17T10:00:00Z"
}
Приклад 2: Upsert (create or replace) з відомим ID
PUT /api/settings/theme HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"key": "theme", "value": "dark", "user_id": 42}
HTTP/1.1 201 Created
Location: /api/settings/theme
PUT vs PATCH: якщо надіслати PUT без поля role, то role буде видалено або скинуто до значення за замовчуванням. Завжди передавайте у PUT всі поля ресурсу, навіть ті, що не змінюються.
Коли використовувати:
- Повна заміна ресурсу відомою структурою
- Upsert операції
- Коли ідемпотентність критична
PATCH оновлює лише вказані поля ресурсу (RFC 5789). Решта полів залишаються незмінними.
Приклад 1: JSON Merge Patch — простий підхід
PATCH /api/users/1 HTTP/1.1
Host: api.example.com
Content-Type: application/merge-patch+json
{
"email": "ivan.updated@example.com"
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1,
"name": "Іван",
"email": "ivan.updated@example.com",
"role": "user"
}
Приклад 2: JSON Patch RFC 6902 — операції над документом
PATCH /api/users/1 HTTP/1.1
Host: api.example.com
Content-Type: application/json-patch+json
[
{"op": "replace", "path": "/email", "value": "new@example.com"},
{"op": "add", "path": "/tags/-", "value": "vip"},
{"op": "remove", "path": "/temp_flag"}
]
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1,
"name": "Іван",
"email": "new@example.com",
"tags": ["user", "vip"]
}
Коли використовувати:
- Зміна одного-двох полів великого об'єкту
- Мобільні клієнти з обмеженим трафіком
- Часткові оновлення без завантаження всього об'єкту
DELETE видаляє ресурс. Ідемпотентний: перший виклик видаляє, наступні — повертають 404.
Приклад 1: Успішне видалення
DELETE /api/users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
HTTP/1.1 204 No Content
Приклад 2: Повторний DELETE (ідемпотентна поведінка)
DELETE /api/users/42 HTTP/1.1
Host: api.example.com
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": "Not Found",
"message": "Користувача з ID 42 не існує"
}
Приклад 3: М'яке видалення (soft delete)
DELETE /api/posts/15 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 15,
"deleted": true,
"deleted_at": "2026-05-17T10:15:00Z"
}
Коли використовувати:
- Видалення ресурсу
- Скасування підписки, відкликання токену
- Очищення сесії (logout)
HEAD ідентичний GET, але сервер не надсилає тіло відповіді. Всі заголовки — ті самі.
Приклад 1: Перевірка існування ресурсу
HEAD /api/users/42 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 284
Last-Modified: Mon, 12 May 2026 08:00:00 GMT
ETag: "a3f8b2c1"
(тіло відсутнє)
Приклад 2: Розмір файлу перед завантаженням
HEAD /files/report-2026.pdf HTTP/1.1
Host: files.example.com
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 5242880
Accept-Ranges: bytes
Клієнт бачить: файл 5MB, підтримує Range — можна завантажувати по частинах.
Коли використовувати:
- Перевірка існування ресурсу без завантаження
- Отримання
Content-Lengthперед завантаженням - Перевірка актуальності кешу (
ETag,Last-Modified) - Легкий health check
OPTIONS повертає список підтримуваних методів для ресурсу.
Приклад 1: Запит можливостей ресурсу
OPTIONS /api/users HTTP/1.1
Host: api.example.com
HTTP/1.1 204 No Content
Allow: GET, POST, HEAD, OPTIONS
Приклад 2: CORS Preflight
OPTIONS /api/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-Id
Access-Control-Max-Age: 86400
Vary: Origin
Коли використовувати:
- Автоматично браузером (CORS preflight) — без вашої участі
- Документування API
- Реалізація CORS-middleware на сервері
HTTP Status Codes: всі п'ять класів
Статус-код — це числова відповідь сервера, що однозначно вказує на результат обробки запиту. Перша цифра визначає клас відповіді, наступні дві — конкретний код.
1xx — Інформаційні
100 Continue
Сервер отримав заголовки запиту і клієнт може надіслати тіло. Використовується з заголовком Expect: 100-continue для великих запитів — клієнт «запитує дозвіл» перед відправкою великого тіла.
POST /upload HTTP/1.1
Host: api.example.com
Content-Length: 10485760
Expect: 100-continue
HTTP/1.1 100 Continue
(після цього клієнт надсилає 10MB тіла)
101 Switching Protocols
Сервер погоджується змінити протокол відповідно до запиту Upgrade. Саме ця відповідь завершує HTTP-рукостискання при переході на WebSocket.
GET /chat HTTP/1.1
Host: ws.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
2xx — Успішні
200 OK
Стандартна успішна відповідь. Тіло залежить від методу: для GET — запитаний ресурс, для POST — результат дії.
GET /api/users/1 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 1, "name": "Іван", "email": "ivan@example.com"}
201 Created
Ресурс успішно створено. Обов'язково містить заголовок Location з URL нового ресурсу.
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"name": "Марія", "email": "maria@example.com"}
HTTP/1.1 201 Created
Location: /api/users/44
Content-Type: application/json
{"id": 44, "name": "Марія", "email": "maria@example.com"}
204 No Content
Успіх, але без тіла відповіді. Типово для DELETE або PUT/PATCH без поверненого ресурсу.
DELETE /api/users/44 HTTP/1.1
Host: api.example.com
HTTP/1.1 204 No Content
206 Partial Content
Відповідь на запит з Range header. Основа для відновлюваних завантажень та відео-стрімінгу.
GET /files/video.mp4 HTTP/1.1
Host: cdn.example.com
Range: bytes=0-1048575
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1048575/52428800
Content-Length: 1048576
Content-Type: video/mp4
(перший 1MB відео...)
3xx — Перенаправлення
301 Moved Permanently
Ресурс назавжди переїхав. Браузери та пошукові роботи кешують назавжди.
GET /old-path HTTP/1.1
Host: example.com
HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-path
304 Not Modified
Ресурс не змінився — клієнт використовує кешовану версію (тіло відсутнє).
GET /api/products HTTP/1.1
Host: api.example.com
If-None-Match: "v5-abc123"
HTTP/1.1 304 Not Modified
ETag: "v5-abc123"
Cache-Control: max-age=60
307 Temporary Redirect
Тимчасовий редирект. Метод і тіло не змінюються — POST залишається POST (на відміну від 302, який часто конвертує POST → GET). Не кешується.
POST /api/v1/upload HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"name": "Андрій"}
HTTP/1.1 307 Temporary Redirect
Location: https://api.example.com/upload-v2
(Клієнт повторить POST на /upload-v2 — метод зберігається)
308 Permanent Redirect
Постійний редирект. Аналог 301, але метод не змінюється. Кешується назавжди.
POST /api/v1/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"name": "Андрій"}
HTTP/1.1 308 Permanent Redirect
Location: https://api.example.com/api/v2/users
(Клієнт повторить POST на /api/v2/users — редирект запам'ятовується браузером)
4xx — Помилки клієнта
Сервер не може обробити запит через синтаксичну помилку: некоректний JSON, відсутнє обов'язкове поле, невалідне значення.
POST /api/users HTTP/1.1
Content-Type: application/json
{"name": "Іван", "email": "not-an-email", "age": "twenty"}
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://example.com/problems/validation",
"title": "Bad Request",
"status": 400,
"errors": {
"email": ["Невалідний формат email"],
"age": ["Очікується число, отримано рядок"]
}
}
Коли: некоректний JSON, відсутні обов'язкові поля, невалідні типи даних.
Клієнт не аутентифікований. Потрібно надати або оновити облікові дані.
GET /api/orders HTTP/1.1
Host: api.example.com
(без Authorization header)
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api.example.com",
error="missing_token",
error_description="Authorization header required"
Content-Type: application/problem+json
{
"type": "https://example.com/problems/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Відсутній або прострочений токен авторизації"
}
Коли: відсутній токен, прострочений JWT, невалідні Basic Auth credentials.
Клієнт аутентифікований, але не має прав на цю операцію.
DELETE /api/users/1 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci... (токен звичайного user, не admin)
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://example.com/problems/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "Видалення користувачів доступне лише адміністраторам"
}
Коли: недостатньо ролей/дозволів, доступ до чужого ресурсу, заблокований IP.
Ресурс не знайдено за вказаним URL.
GET /api/users/99999 HTTP/1.1
Host: api.example.com
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://example.com/problems/not-found",
"title": "Not Found",
"status": 404,
"detail": "Користувача з ID 99999 не існує"
}
Коли: ресурс видалений, неправильний ID, помилка в URL. Також використовується замість 403 для приховування існування ресурсу.
Конфлікт із поточним станом ресурсу.
POST /api/users HTTP/1.1
Content-Type: application/json
{"name": "Іван", "email": "ivan@example.com"}
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": "Conflict",
"message": "Користувач з таким email вже існує",
"conflictingField": "email"
}
Також: optimistic concurrency conflict:
PUT /api/posts/5 HTTP/1.1
If-Match: "old-etag-value"
HTTP/1.1 409 Conflict
{"error": "Resource was modified by another request. Please reload."}
Коли: дублікат унікального поля, конфлікт версій (optimistic locking), спроба перевести у несумісний стан.
Синтаксис JSON правильний, але семантика невалідна — бізнес-правила порушено.
POST /api/orders HTTP/1.1
Content-Type: application/json
{
"product_id": 42,
"quantity": -5,
"delivery_date": "2020-01-01"
}
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"title": "Unprocessable Entity",
"status": 422,
"errors": {
"quantity": ["Кількість повинна бути більше 0"],
"delivery_date": ["Дата доставки не може бути в минулому"]
}
}
Коли: порушення бізнес-правил, семантично некоректні дані, валідаційні помилки у REST API.
Клієнт перевищив ліміт запитів (rate limiting).
GET /api/search?q=test HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1747480800
Content-Type: application/json
{
"error": "Too Many Requests",
"message": "Перевищено ліміт: 100 запитів на хвилину",
"retry_after": 60
}
Коли: rate limiting по IP або токену, захист від DDoS, обмеження free tier.
5xx — Помилки сервера
Необроблений виняток у коді сервера.
GET /api/report/generate HTTP/1.1
Host: api.example.com
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
{
"type": "https://example.com/problems/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "Сталася внутрішня помилка сервера",
"trace_id": "a3f8b2c1-d9e2-4f1a"
}
Увага: у production ніколи не розкривайте stack trace у відповіді — лише trace_id для пошуку у логах.
Проксі або gateway отримав невалідну відповідь від upstream сервера.
HTTP/1.1 502 Bad Gateway
Content-Type: text/html
<html><body>Bad Gateway: upstream server returned 502</body></html>
Коли: nginx не може достукатись до FastAPI/Node.js backend, мікросервіс повернув некоректну відповідь.
Сервер тимчасово недоступний — перевантажений або на обслуговуванні.
HTTP/1.1 503 Service Unavailable
Retry-After: 300
Content-Type: application/json
{
"error": "Service Unavailable",
"message": "Технічне обслуговування до 12:00 UTC",
"retry_after": 300
}
Коли: планове обслуговування, перевантаження, circuit breaker відкрито.
Проксі не отримав відповідь від upstream за відведений час.
HTTP/1.1 504 Gateway Timeout
Content-Type: application/json
{
"error": "Gateway Timeout",
"message": "Upstream server не відповів протягом 30 секунд"
}
Коли: повільний database query, зависла зовнішня API, мікросервіс завантажений.
HTTP Headers: детальний розбір
Заголовки — це метадані HTTP-повідомлення. Вони несуть інформацію про формат тіла, аутентифікацію, кешування, кодування, а також власні розширення застосунку. Заголовки розділяються на кілька категорій.
Найважливіші заголовки запиту
Host: api.example.com або Host: localhost:5000. Обов'язковий починаючи з HTTP/1.1 — без нього сервер не знає, для якого віртуального хосту призначений запит. Єдиний обов'язковий заголовок у HTTP/1.1.Accept: text/html, application/json;q=0.9, */*;q=0.8
Сервер обирає найкращий доступний формат — це content negotiation.Content-Type: application/json— JSONContent-Type: application/x-www-form-urlencoded— HTML-формаContent-Type: multipart/form-data; boundary=----...— форма з файлами
Authorization: Basic dXNlcjpwYXNz— Base64 (login:password)Authorization: Bearer eyJhbGc...— JWT або OAuth токенAuthorization: Digest ...— Digest Auth
Accept-Encoding: gzip, deflate, br. Сервер може стиснути відповідь і вказати Content-Encoding: gzip. Браузери завжди підтримують gzip та brotli.304 Not Modified без тіла. Це основа HTTP-кешування.User-Agent: dotnet-httpclient/8.0.Найважливіші заголовки відповіді
Content-Type: application/json; charset=utf-8Content-Type: text/html; charset=utf-8Content-Type: image/webpContent-Type: application/octet-stream— довільні бінарні дані
Transfer-Encoding: chunked для потокової передачі. При Content-Length клієнт знає заздалегідь, скільки читати.no-cache— перевіряти актуальність перед використанням кешуno-store— ніколи не кешувати (особисті дані)max-age=3600— кешувати 3600 секундpublic— можна кешувати у CDN та проксіprivate— тільки у браузері користувача
ETag: "33a64df5". Клієнт зберігає і передає у наступному запиті як If-None-Match. Якщо збігається — 304 Not Modified. Якщо ні — нова версія ресурсу.3xx) або URL нового ресурсу (201 Created). При 301/302 браузер автоматично переходить за цим URL.Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600; Path=/401 Unauthorized. Вказує, яку схему аутентифікації очікує сервер:
WWW-Authenticate: Bearer realm="api.example.com", error="invalid_token"X- (застаріла конвенція, RFC 6648 скасував її у 2012) або просто описові назви: X-Request-Id, X-Rate-Limit-Remaining. Всі популярні фреймворки додають власні заголовки: X-Powered-By, Server, тощо. У production рекомендується приховуватиServer заголовок з міркувань безпеки.HTTP у .NET: екосистема HttpClient
Огляд архітектури
Платформа .NET надає кілька рівнів абстракції для роботи з HTTP. Розуміння їх взаємозв'язку є ключем до правильного вибору інструменту.
Клас HttpClient: детальний розбір
HttpClient — основний клас для HTTP-запитів у .NET. Він не є безпечним для повторного створення на кожен запит — правильна практика: один екземпляр на час життя застосунку (або через IHttpClientFactory).
using var client = new HttpClient()
Якщо створювати новий HttpClient для кожного запиту через using, виникає socket exhaustion — вичерпання портів. HttpClient утримує TCP-з'єднання у стані TIME_WAIT ще кілька хвилин після Dispose(). При великому навантаженні це призводить до помилки SocketException: Only one usage of each socket address is permitted.client.BaseAddress = new Uri("https://api.example.com/v1/"). Дозволяє писати client.GetAsync("users") замість повної URL.Authorization, Accept, User-Agent.TaskCanceledException. Для різних операцій можна передавати CancellationToken безпосередньо.Основні методи та їх використання
using System.Net.Http.Json;
// ✅ Правильно: HttpClient як singleton або через DI
using var client = new HttpClient
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/")
};
// GET з десеріалізацією JSON у один рядок
Todo? todo = await client.GetFromJsonAsync<Todo>("todos/1");
Console.WriteLine($"Title: {todo?.Title}");
// GET зі перевіркою статус-коду
HttpResponseMessage response = await client.GetAsync("todos/999");
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
Console.WriteLine("Не знайдено!");
return;
}
// EnsureSuccessStatusCode() кидає HttpRequestException при 4xx/5xx
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync();
Console.WriteLine(json);
record Todo(int Id, string Title, bool Completed);
using System.Net.Http.Json;
using var client = new HttpClient
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/")
};
// POST: серіалізація об'єкта в JSON автоматично
var newPost = new CreatePostRequest("My Title", "Body text", 1);
// PostAsJsonAsync серіалізує і встановлює Content-Type: application/json
HttpResponseMessage response = await client.PostAsJsonAsync("posts", newPost);
response.EnsureSuccessStatusCode();
// Десеріалізація відповіді
Post? created = await response.Content.ReadFromJsonAsync<Post>();
Console.WriteLine($"Created with ID: {created?.Id}");
record CreatePostRequest(string Title, string Body, int UserId);
record Post(int Id, string Title, string Body, int UserId);
using System.Net.Http.Json;
using var client = new HttpClient
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/")
};
// PUT — повна заміна
var updated = new Post(1, "New Title", "New body", 1);
var putResponse = await client.PutAsJsonAsync("posts/1", updated);
putResponse.EnsureSuccessStatusCode();
// PATCH — часткове оновлення (через HttpRequestMessage для PATCH)
var patch = new { Title = "Patched Title" };
var patchContent = JsonContent.Create(patch);
var patchResponse = await client.PatchAsync("posts/1", patchContent);
patchResponse.EnsureSuccessStatusCode();
// DELETE
HttpResponseMessage deleteResponse = await client.DeleteAsync("posts/1");
Console.WriteLine($"Delete: {deleteResponse.StatusCode}"); // 200 OK
record Post(int Id, string Title, string Body, int UserId);
using System.Net.Http.Headers;
using var client = new HttpClient();
// Створюємо запит вручну для повного контролю
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/data")
{
// Версія HTTP
Version = new Version(2, 0),
VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
// Заголовки специфічні для цього запиту
Headers =
{
{ "X-Request-Id", Guid.NewGuid().ToString() },
Accept = { new MediaTypeWithQualityHeaderValue("application/json") }
}
};
// Додаємо Authorization (тільки для цього запиту)
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", "eyJhbGci...");
using HttpResponseMessage response = await client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead, // не читаємо тіло одразу
CancellationToken.None
);
// Потокове читання великого тіла
await using Stream stream = await response.Content.ReadAsStreamAsync();
// ... обробка stream
IHttpClientFactory: правильний спосіб в DI-застосунках
У застосунках на основі Microsoft.Extensions.DependencyInjection (ASP.NET Core, Worker Services) слід використовувати IHttpClientFactory замість прямого new HttpClient().
// Program.cs — реєстрація іменованого клієнта
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Реєстрація іменованого HTTP-клієнта
builder.Services.AddHttpClient("JsonPlaceholder", client =>
{
client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(30);
});
// Або типізований клієнт (рекомендовано для великих застосунків)
builder.Services.AddHttpClient<ITodoService, TodoService>(client =>
{
client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
});
var app = builder.Build();
// ── У сервісі ────────────────────────────────────────────────────────────────
public interface ITodoService
{
Task<Todo[]> GetAllAsync(CancellationToken ct = default);
}
public sealed class TodoService(HttpClient client) : ITodoService
{
// HttpClient вже налаштований через AddHttpClient<ITodoService, TodoService>
public async Task<Todo[]> GetAllAsync(CancellationToken ct = default)
{
return await client.GetFromJsonAsync<Todo[]>("todos", ct) ?? [];
}
}
record Todo(int Id, int UserId, string Title, bool Completed);
Практичний проєкт: Консольний HTTP-клієнт
Побудуємо простий консольний застосунок, що демонструє всі основні HTTP-операції. Мета — побачити HTTP у дії: реальні запити, реальні статус-коди, реальні відповіді. Ніяких складних патернів — лише HttpClient і публічний API JSONPlaceholder.
GET, POST, PUT, PATCH, DELETE для ресурсів /posts, /users, /todos, /comments. Зміни не зберігаються на сервері — ідеально для навчання.Що будуємо
Кроки
Створюємо проєкт
dotnet new console -n PostsExplorer
cd PostsExplorer
Замінюємо Program.cs
Вставте код нижче у Program.cs — більше жодних файлів не потрібно.
Запускаємо
dotnet run
Program.cs
// Program.cs — Posts Explorer
// Демонструє всі HTTP-методи через HttpClient на JSONPlaceholder API
using System.Net;
using System.Net.Http.Json;
// ── Один HttpClient на весь застосунок ──────────────────────────────────────
// Правило: не створюйте new HttpClient() для кожного запиту!
// Це призводить до вичерпання портів (socket exhaustion).
var http = new HttpClient
{
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/"),
Timeout = TimeSpan.FromSeconds(15)
};
// Content negotiation: кажемо серверу, що приймаємо JSON
http.DefaultRequestHeaders.Add("Accept", "application/json");
Header("Posts Explorer — HTTP Demo");
// ══════════════════════════════════════════════════════════════════════════════
// 1. GET /posts — отримати колекцію
// HTTP GET: безпечний + ідемпотентний, без тіла запиту
// ══════════════════════════════════════════════════════════════════════════════
Section("1. GET /posts — список постів");
HttpResponseMessage r1 = await http.GetAsync("posts");
PrintStatus(r1);
Post[]? allPosts = await r1.Content.ReadFromJsonAsync<Post[]>();
Console.WriteLine($" Отримано постів: {allPosts?.Length}. Перші 3:");
foreach (Post p in allPosts?.Take(3) ?? [])
Console.WriteLine($" [{p.Id,3}] {p.Title[..Math.Min(50, p.Title.Length)]}");
// ══════════════════════════════════════════════════════════════════════════════
// 2. GET /posts/1 — отримати конкретний ресурс
// ══════════════════════════════════════════════════════════════════════════════
Section("2. GET /posts/1 — один ресурс");
HttpResponseMessage r2 = await http.GetAsync("posts/1");
PrintStatus(r2);
Post? single = await r2.Content.ReadFromJsonAsync<Post>();
Console.WriteLine($" Title: {single?.Title}");
Console.WriteLine($" Body: {single?.Body[..50]}...");
// ══════════════════════════════════════════════════════════════════════════════
// 3. GET /posts/9999 — ресурс не існує → 404
// Обробляємо 404 явно, без виключення
// ══════════════════════════════════════════════════════════════════════════════
Section("3. GET /posts/9999 — обробка 404");
HttpResponseMessage r3 = await http.GetAsync("posts/9999");
PrintStatus(r3);
if (r3.StatusCode == HttpStatusCode.NotFound)
Console.WriteLine(" → Ресурс не знайдено. 404 оброблено gracefully.");
// ══════════════════════════════════════════════════════════════════════════════
// 4. POST /posts — створити ресурс
// HTTP POST: не ідемпотентний, сервер повертає 201 + Location header
// ══════════════════════════════════════════════════════════════════════════════
Section("4. POST /posts — створити пост");
var newPost = new PostInput("Мій перший HTTP-пост", "Навчаюсь HTTP в C#!", UserId: 1);
HttpResponseMessage r4 = await http.PostAsJsonAsync("posts", newPost);
PrintStatus(r4);
// 201 Created: перевіряємо Location header з URL нового ресурсу
Console.WriteLine($" Location: {r4.Headers.Location}");
Post? created = await r4.Content.ReadFromJsonAsync<Post>();
Console.WriteLine($" Новий ID: {created?.Id}");
// ══════════════════════════════════════════════════════════════════════════════
// 5. PUT /posts/1 — повна заміна ресурсу
// HTTP PUT: ідемпотентний, передаємо ВСІ поля
// ══════════════════════════════════════════════════════════════════════════════
Section("5. PUT /posts/1 — повна заміна");
var replacement = new Post(1, 1, "Повністю замінений заголовок", "Весь текст змінено");
HttpResponseMessage r5 = await http.PutAsJsonAsync("posts/1", replacement);
PrintStatus(r5);
Post? replaced = await r5.Content.ReadFromJsonAsync<Post>();
Console.WriteLine($" Title: {replaced?.Title}");
// ══════════════════════════════════════════════════════════════════════════════
// 6. PATCH /posts/1 — часткове оновлення
// Надсилаємо ТІЛЬКИ поля, що змінились
// ══════════════════════════════════════════════════════════════════════════════
Section("6. PATCH /posts/1 — оновити лише title");
var patch = new { title = "Оновлений через PATCH" };
HttpResponseMessage r6 = await http.PatchAsync("posts/1", JsonContent.Create(patch));
PrintStatus(r6);
Post? patched = await r6.Content.ReadFromJsonAsync<Post>();
Console.WriteLine($" Title: {patched?.Title}");
Console.WriteLine($" Body залишився: {patched?.Body[..40]}...");
// ══════════════════════════════════════════════════════════════════════════════
// 7. DELETE /posts/1 — видалити ресурс
// HTTP DELETE: ідемпотентний — повторний виклик не змінить стан
// ══════════════════════════════════════════════════════════════════════════════
Section("7. DELETE /posts/1 — видалити");
HttpResponseMessage r7 = await http.DeleteAsync("posts/1");
PrintStatus(r7);
Console.WriteLine(r7.IsSuccessStatusCode ? " ✓ Видалено" : " ✗ Помилка");
Header("Готово! Всі HTTP-методи виконано.");
// ── Допоміжні методи ──────────────────────────────────────────────────────────
void Header(string msg)
{
Console.WriteLine();
Console.WriteLine(new string('═', 48));
Console.WriteLine($" {msg}");
Console.WriteLine(new string('═', 48));
}
void Section(string title)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"▶ {title}");
Console.ResetColor();
}
void PrintStatus(HttpResponseMessage response)
{
int code = (int)response.StatusCode;
Console.ForegroundColor = code switch
{
>= 200 and < 300 => ConsoleColor.Green,
>= 300 and < 400 => ConsoleColor.Yellow,
_ => ConsoleColor.Red
};
Console.Write($" HTTP {code} {response.StatusCode}");
Console.ResetColor();
Console.WriteLine($" ← {response.RequestMessage?.Method} {response.RequestMessage?.RequestUri?.PathAndQuery}");
}
// ── Моделі ────────────────────────────────────────────────────────────────────
record Post(int Id, int UserId, string Title, string Body);
record PostInput(string Title, string Body, int UserId);
Вивід
Що ви щойно побудували
HTTP-методи
GET— безпечний, ідемпотентний, без тілаPOST→201 Created+LocationheaderPUT— ідемпотентний, повна заміна всіх полівPATCH— лише поля, що змінилисьDELETE— ідемпотентний
Статус-коди
200 OK— успішна операція201 Created— ресурс створено404 Not Found— обробка без виключеньIsSuccessStatusCodevsEnsureSuccessStatusCode()
HttpClient у C#
- Один екземпляр для всіх запитів
PostAsJsonAsync/GetFromJsonAsync— серіалізація автоматичнаJsonContent.Create— для PATCH і кастомних запитівHeaders.Location— читання заголовків відповіді
posts на users, todos або comments — JSONPlaceholder підтримує всі ці ресурси. Спробуйте навмисно надіслати некоректні дані і подивіться, яку відповідь повертає сервер.Практика та закріплення
Теоретичний розбір HTTP легко створює оманливе відчуття зрозумілості — поки не спробуєш написати реальний клієнт. Наведені нижче завдання побудовані за принципом поступового ускладнення: від базового розуміння структури повідомлень до проектування власного HTTP-клієнта з підтримкою аутентифікації та повторних запитів.
Рівень 1. Базове розуміння
- Напишіть власними словами різницю між ідемпотентним та безпечним HTTP-методом. Наведіть по два приклади для кожної категорії.
- Яку HTTP-відповідь поверне сервер у кожному з цих сценаріїв? Обґрунтуйте вибір статус-коду:
- Клієнт запитує ресурс, який не існує
- Клієнт успішно створив новий об'єкт
- Клієнт надіслав JSON з пропущеним обов'язковим полем
- Клієнт намагається видалити об'єкт, що належить іншому користувачу
- Сервер впав через непередбачений виняток у коді
- Яка різниця між заголовками
Authorization: Basic ...таAuthorization: Bearer ...? В яких сценаріях застосовується кожен?
Рівень 2. Робота з HttpClient
- Напишіть консольний застосунок, що:
- Отримує список постів з
jsonplaceholder.typicode.com/posts - Фільтрує пости з
userId = 1 - Виводить
Idта перші 50 символівTitleкожного поста - Виводить загальну кількість знайдених постів
- Отримує список постів з
- Реалізуйте
HEAD-запит для перевірки існування ресурсу без завантаження тіла. Порівняйте час виконанняHEADтаGETдля одного ресурсу за допомогоюStopwatch. - Напишіть метод
DownloadWithProgressAsync(string url, string filePath), що:- Завантажує файл за URL з підтримкою великих файлів (потокове читання через
ReadAsStreamAsync) - Виводить прогрес у відсотках (якщо сервер повертає
Content-Length) - Не завантажує весь файл у RAM одночасно
- Завантажує файл за URL з підтримкою великих файлів (потокове читання через
Рівень 3. Архітектурне мислення
- Реалізуйте
CircuitBreakerHandler : DelegatingHandler, що:- Відстежує кількість поспіль невдалих запитів (5xx або
TaskCanceledException) - При 5 невдачах поспіль переходить у стан «Відкритий» (Open) — відхиляє нові запити без надсилання
- Через 30 секунд переходить у «Напіввідкритий» (Half-Open) — пропускає один тестовий запит
- При успішному тестовому запиті повертається у «Закритий» (Closed)
- Використайте PlantUML state diagram для документування станів
- Відстежує кількість поспіль невдалих запитів (5xx або
- Спроектуйте типізований
GitHubApiClientіз підтримкою:- Rate limiting (заголовки
X-RateLimit-Remaining,X-RateLimit-Reset) - Автоматичного очікування при вичерпанні ліміту
- Pagination через
Linkheader - Bearer token аутентифікації через
IOptions<GitHubOptions>
- Rate limiting (заголовки
Контрольні питання
Нижче подано набір запитань для самоперевірки. Якщо на будь-яке з них важко відповісти без повторного читання — це нормальний сигнал повернутись до відповідного розділу.
- Чим відрізняється HTTP від TCP? Чому HTTP не може існувати без TCP (або QUIC)?
- Що таке
statelessі як це фундаментально впливає на архітектуру вебзастосунків? - Яку мінімальну кількість заголовків обов'язково містить валідний HTTP/1.1 запит?
- В чому відмінність між
PUTтаPATCH? Наведіть сценарій, де важливо вибрати правильно. - Чому
401 Unauthorized— погана назва? Яка семантична різниця між401та403? - Що таке socket exhaustion у
HttpClientі якIHttpClientFactoryце вирішує? - Навіщо потрібен
DelegatingHandler? Наведіть три реальних сценарії його використання. - Що відбудеться, якщо сервер поверне
301 Moved Permanentlyу відповідь наPOST-запит? Що відбудеться при308? - Для чого використовується
HEAD-метод? Наведіть два практичних прикладів. - Що означає
ETagі як він пов'язаний зі статус-кодом304 Not Modified?
UDP Broadcast та Multicast
Детальний розбір механізмів групової розсилки UDP — Broadcast та Multicast. Теорія, адресний простір, практичний приклад сервісу виявлення пристроїв у локальній мережі на C#.
HttpListener — вбудований HTTP-сервер .NET
Детальний розгляд класу HttpListener — нативного HTTP-сервера .NET без ASP.NET Core. Архітектура, повний API, конфігурація, аутентифікація, обробка запитів та два практичні приклади — статичний файловий сервер та REST API.