Network Programming

HTTP — протокол вебу

Глибоке вивчення HTTP — від структури повідомлень та методів до повноцінного REST API клієнта на C#. Академічний розбір RFC 7230–7235, анатомія запитів і відповідей, статус-коди, заголовки та HttpClient екосистема .NET.

HTTP — протокол вебу

Від теорії до практики: чому HTTP існує

У попередніх розділах ми вивчили транспортний рівень — UDP та TCP. Обидва протоколи вирішують задачу доставки байтів між процесами. Але що робити з цими байтами? Який сенс несуть вони для застосунку? Відповідь на це питання дає прикладний рівень (Application Layer, Layer 7 моделі OSI).

Саме на прикладному рівні живе HTTP — HyperText Transfer Protocol. Він визначає не те, як байти доставляються (це справа TCP), а що вони означають: який ресурс запитується, яким методом, яка відповідь очікується і що вона містить.

HTTP є основою World Wide Web і одним з найважливіших протоколів в історії комп'ютерних мереж. Кожного разу, коли ви відкриваєте браузер, кожен REST API виклик, кожне завантаження файлу — все це HTTP.

Ключова ідея цього розділу: HTTP — це протокол прикладного рівня, що будується поверх TCP. Він визначає семантику запитів і відповідей: що саме клієнт хоче отримати, і що саме сервер зобов'язаний повернути. Розуміння HTTP є обов'язковою навичкою для будь-якого сучасного розробника.

Історія та еволюція стандарту

HTTP пройшов довгий шлях від однорядкового протоколу до складної багаторівневої специфікації. Розуміння цього шляху пояснює багато сучасних рішень і «дивних» деталей протоколу.

Loading diagram...
@startmindmap
skinparam style plain
skinparam backgroundColor #ffffff

* HTTP
** HTTP/0.9 (1991)
*** Тім Бернерс-Лі, CERN
*** Лише GET
*** Лише HTML у відповіді
*** Без заголовків
** HTTP/1.0 (1996)
*** RFC 1945
*** Додано заголовки
*** POST, HEAD методи
*** Нове з'єднання на кожен запит
** HTTP/1.1 (1997)
*** RFC 2068 → RFC 7230–7235
*** Keep-Alive за замовчуванням
*** Chunked Transfer Encoding
*** Віртуальний хостинг (Host header)
*** Pipelining (погано працює на практиці)
** HTTP/2 (2015)
*** RFC 7540
*** Бінарний протокол
*** Мультиплексування потоків
*** Стиснення заголовків HPACK
*** Server Push
** HTTP/3 (2022)
*** RFC 9114
*** Поверх QUIC (UDP)
*** 0-RTT з'єднання
*** Усунення HOL blocking
@endmindmap
RFC 7230 — офіційне визначення HTTP/1.1: "The Hypertext Transfer Protocol (HTTP) is a stateless application-level protocol for distributed, collaborative, hypertext information systems."

Зверніть увагу на ключове слово 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 будує власний механізм надійності на рівні потоків, тому втрата пакету одного потоку не блокує інші.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "HTTP/1.0\n(1996)" as h10 #fce4ec {
    rectangle "Нове TCP\nна кожен запит" as c10 #f48fb1
}

rectangle "HTTP/1.1\n(1997)" as h11 #fff9c4 {
    rectangle "Keep-Alive\nPipelining (теор.)" as c11 #ffe082
}

rectangle "HTTP/2\n(2015)" as h2 #e3f2fd {
    rectangle "Мультиплексинг\nHPACK\nServer Push" as c2 #90caf9
}

rectangle "HTTP/3\n(2022)" as h3 #e8f5e9 {
    rectangle "QUIC (UDP)\n0-RTT\nПовне усунення\nHOL blocking" as c3 #a5d6a7
}

h10 -right-> h11 : Keep-Alive
h11 -right-> h2 : Бінарний протокол
h2 -right-> h3 : Замінює TCP

note bottom of h10
  31 з'єднання
  для 1 сторінки
  з 30 img
end note

note bottom of h11
  1 з'єднання,
  але черга запитів
end note

note bottom of h2
  Паралельні потоки,
  але TCP HOL ще є
end note

note bottom of h3
  Повна паралельність,
  навіть при втраті пакетів
end note

@enduml

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
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "URL" #f5f5f5 {
    rectangle "scheme\nhttps" as scheme #e3f2fd
    rectangle "://" as sep1 #f5f5f5
    rectangle "userinfo\nuser:pass" as user #fff9c4
    rectangle "@" as sep2 #f5f5f5
    rectangle "host\napi.example.com" as host #e8f5e9
    rectangle ":" as sep3 #f5f5f5
    rectangle "port\n8443" as port #fce4ec
    rectangle "path\n/v1/users/42" as path #fff3e0
    rectangle "?" as sep4 #f5f5f5
    rectangle "query\nrole=admin&page=2" as query #f3e5f5
    rectangle "#" as sep5 #f5f5f5
    rectangle "fragment\nprofile" as frag #e0f7fa

    scheme -[hidden]right-> sep1
    sep1 -[hidden]right-> user
    user -[hidden]right-> sep2
    sep2 -[hidden]right-> host
    host -[hidden]right-> sep3
    sep3 -[hidden]right-> port
    port -[hidden]right-> path
    path -[hidden]right-> sep4
    sep4 -[hidden]right-> query
    query -[hidden]right-> sep5
    sep5 -[hidden]right-> frag
}

note bottom of scheme
  http або https.
  Визначає протокол.
end note

note bottom of host
  Доменне ім'я або IP.
  Резолвиться через DNS.
end note

note bottom of port
  За замовчуванням:
  http → 80
  https → 443
end note

note bottom of path
  Ієрархічний
  ідентифікатор
  ресурсу.
end note

note bottom of query
  Параметри фільтрації,
  сортування, пагінації.
  key=value&key2=value2
end note

note bottom of frag
  НЕ надсилається
  серверу!
  Тільки браузер.
end note

@enduml

Детальний розбір кожної компоненти

Scheme (протокол)
string (обов'язковий)
Визначає протокол доступу до ресурсу. Реєстронезалежний. Поширені схеми:
  • http — незахищений HTTP (порт 80 за замовчуванням)
  • https — HTTP over TLS (порт 443 за замовчуванням)
  • ftp — File Transfer Protocol (порт 21)
  • ws / wss — WebSocket (незахищений / захищений)
  • mailto — email-адреса (не HTTP)
  • file — локальна файлова система
Authority = [userinfo@] host [:port]
string
Userinfo (user:password@) — базова автентифікація в URL. Застаріла практика — паролі видно в логах та адресному рядку. Ніколи не використовуйте у production.
Host
string
Host — доменне ім'я (api.example.com) або IPv4 (192.168.1.1) або IPv6 у дужках ([::1]). Регістронезалежний. Резолвиться через DNS у IP-адресу.
Port
number
Port — число від 1 до 65535. Якщо не вказано — використовується порт за замовчуванням для схеми. https://example.com:443/ = https://example.com/ (443 — порт за замовчуванням для HTTPS).
Path (шлях)
string
Ієрархічний ідентифікатор ресурсу. Починається з /. Компоненти розділяються /.Правила:
  • %XX — URL-кодування небезпечних символів (пробіл → %20 або + у query)
  • .. — піддиректорія вгору (сервери повинні нормалізувати!)
  • Регістрочутливий на Unix-серверах, регістронезалежний на Windows IIS
Паттерни REST API:
  • /users — колекція
  • /users/42 — конкретний ресурс за ID
  • /users/42/orders — вкладений ресурс
  • /users/42/orders/7/items — глибоко вкладений
Query String (рядок запиту)
key=value пари
Параметри, що передаються після ?. Пари 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 (застаріло)
Fragment (фрагмент)
string
Частина після #. Ніколи не надсилається серверу — обробляється виключно браузером. Використовується для:
  • Навігації до розділу сторінки (#section-headers)
  • Single Page Application routing (#/users/42)
  • OAuth redirect URI state (#access_token=...)
Сервер взагалі не знає, який фрагмент вказав клієнт.

URL vs URI vs URN

Ці терміни часто плутають:

ТермінРозшифровкаЩо ідентифікуєПриклад
URIUniform Resource IdentifierБудь-який ресурсhttps://..., urn:isbn:...
URLUniform Resource LocatorРесурс + де знаходитьсяhttps://api.example.com/users
URNUniform 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

Перш ніж розбирати деталі протоколу, критично важливо розуміти, де саме він знаходиться у стеці мережевих протоколів та на чому базується.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "Прикладний рівень (Application)" #e8f5e9 {
    rectangle "HTTP / HTTPS" as http #a5d6a7
    rectangle "WebSocket / gRPC / GraphQL" as ws #c8e6c9
}

rectangle "Транспортний рівень (Transport)" #e3f2fd {
    rectangle "TCP (надійна доставка)" as tcp #90caf9
    rectangle "QUIC (HTTP/3)" as quic #bbdefb
}

rectangle "Мережевий рівень (Network)" #fff3e0 {
    rectangle "IP (адресація та маршрутизація)" as ip #ffe0b2
}

rectangle "Канальний рівень (Data Link)" #fce4ec {
    rectangle "Ethernet / Wi-Fi" as eth #f48fb1
}

http -down-> tcp : поверх TCP\n(порт 80/443)
ws -down-> tcp
quic -down-> ip : поверх UDP
tcp -down-> ip
ip -down-> eth

note right of http
    HTTP/1.1 та HTTP/2
    використовують TCP.
    HTTP/3 — QUIC (UDP).
end note

@enduml
РівеньПротоколВідповідає за
ПрикладнийHTTPСемантика запитів і відповідей
ТранспортнийTCPНадійна доставка, порядок, без втрат
МережевийIPМаршрутизація між мережами
КанальнийEthernet/Wi-FiДоставка в межах одного сегменту
Практичне значення для розробника: коли ваш HTTP-запит «зависає», проблема може бути на будь-якому рівні. DNS не резолвиться (прикладний рівень), TCP SYN не отримав SYN-ACK (транспортний), маршрут недоступний (мережевий), або WiFi погано працює (канальний). Розуміння стеку допомагає правильно діагностувати.

Анатомія HTTP-повідомлення

HTTP-повідомлення бувають двох типів: запит (Request) від клієнта до сервера та відповідь (Response) від сервера до клієнта. Обидва мають однакову загальну структуру, але різні перші рядки.

Структура HTTP-запиту

Кожен HTTP-запит складається з трьох частин: стартового рядка, заголовків та (опціонально) тіла.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "HTTP Request" #fff3e0 {
    rectangle "Request Line" as rl #ffe0b2 {
        rectangle "Method\nGET" as m #ffcc80
        rectangle "Request Target\n/api/users?page=1" as rt #ffcc80
        rectangle "HTTP Version\nHTTP/1.1" as v #ffcc80
        m -[hidden]right-> rt
        rt -[hidden]right-> v
    }
    rectangle "Headers" as h #e3f2fd {
        rectangle "Host: api.example.com" as h1 #bbdefb
        rectangle "Accept: application/json" as h2 #bbdefb
        rectangle "Authorization: Bearer eyJ..." as h3 #bbdefb
        rectangle "Content-Type: application/json" as h4 #bbdefb
        h1 -[hidden]down-> h2
        h2 -[hidden]down-> h3
        h3 -[hidden]down-> h4
    }
    rectangle "Empty Line (CRLF)" as el #f5f5f5
    rectangle "Body (payload)\n{\"name\": \"Alice\"}" as b #e8f5e9
    rl -[hidden]down-> h
    h -[hidden]down-> el
    el -[hidden]down-> b
}

note right of rl
  Обов'язковий.
  Визначає метод,
  ресурс і версію.
end note

note right of h
  Один або більше.
  Формат: "Name: Value CRLF"
  Закінчуються порожнім рядком.
end note

note right of b
  Опціональне.
  Є у POST, PUT, PATCH.
  Відсутнє у GET, HEAD, DELETE.
end note

@enduml

У «сирому» вигляді (як передається по 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>
Порожній рядок після заголовків є обов'язковим — він сигналізує серверу, що всі заголовки передані. Якщо є тіло (наприклад, у POST-запиті), воно іде безпосередньо після цього порожнього рядка.

Структура HTTP-відповіді

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "HTTP Response" #e8f5e9 {
    rectangle "Status Line" as sl #c8e6c9 {
        rectangle "HTTP Version\nHTTP/1.1" as v #a5d6a7
        rectangle "Status Code\n200" as sc #a5d6a7
        rectangle "Reason Phrase\nOK" as rp #a5d6a7
        v -[hidden]right-> sc
        sc -[hidden]right-> rp
    }
    rectangle "Response Headers" as rh #e3f2fd {
        rectangle "Content-Type: application/json; charset=utf-8" as rh1 #bbdefb
        rectangle "Content-Length: 348" as rh2 #bbdefb
        rectangle "Cache-Control: max-age=3600" as rh3 #bbdefb
        rectangle "X-Request-Id: a3f8b2c1" as rh4 #bbdefb
        rh1 -[hidden]down-> rh2
        rh2 -[hidden]down-> rh3
        rh3 -[hidden]down-> rh4
    }
    rectangle "Empty Line (CRLF)" as el #f5f5f5
    rectangle "Response Body\n[{\"id\":1,\"name\":\"Alice\"}, ...]" as rb #fff9c4
    sl -[hidden]down-> rh
    rh -[hidden]down-> el
    el -[hidden]down-> rb
}

@enduml
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-Version
string
Версія протоколу: HTTP/1.0, HTTP/1.1 або HTTP/2. У HTTP/2 статусний рядок передається у бінарному форматі через псевдо-заголовок :status, але семантика залишається тією самою.
Status-Code
uint16 (3 цифри)
Тризначний числовий код, що вказує на результат обробки запиту. Перша цифра визначає клас відповіді: 1xx — інформаційні, 2xx — успішні, 3xx — редиректи, 4xx — помилки клієнта, 5xx — помилки сервера.
Reason-Phrase
string
Текстовий опис статус-коду для людини: OK, Not Found, Internal Server Error. У HTTP/2 цей рядок відсутній — лише числовий код. Застосунки не повинні покладатись на reason phrase у своїй логіці.
Headers
key: value пари
Метадані повідомлення. Заголовки регістронезалежні (Content-Type = content-type). Один заголовок може мати кілька значень, розділених комою: Accept: text/html, application/json.
Body
byte[]
Корисне навантаження відповіді. Формат визначається заголовком Content-Type. Розмір — заголовком Content-Length (або через Transfer-Encoding: chunked для потокової передачі невідомого розміру).

HTTP в дії: повні сценарії запит–відповідь

Подивімося, як виглядають реальні HTTP-діалоги між клієнтом і сервером. Саме такий текст передається по TCP-з'єднанню — символ за символом, без жодних абстракцій.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Клієнт\n(браузер / HttpClient)" as client
participant "Сервер" as server #e8f5e9

client -> server : TCP з'єднання\n(3-way handshake)
note right : SYN → SYN-ACK → ACK

client -> server : HTTP Request\n(текст через TCP)
note left
  GET /api/users/1 HTTP/1.1
  Host: api.example.com
  Accept: application/json
  [порожній рядок]
end note

server --> client : HTTP Response\n(текст через TCP)
note right
  HTTP/1.1 200 OK
  Content-Type: application/json
  [порожній рядок]
  {"id":1,"name":"Alice"}
end note

@enduml
HTTP — це текстовий протокол. Кожен запит і відповідь — це набір ASCII-рядків, що передаються через 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

Зверніть увагу на порожній рядок наприкінці — він обов'язковий. Саме він сигналізує серверу, що всі заголовки передано.

Сценарій 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 вказує серверу, скільки байтів читати.

Сценарій 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 — успіх без тіла. Ресурс видалено.

Сценарій 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.

Мінімальний 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 — ідемпотентний, але не безпечний.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

class "GET" as GET #a5d6a7 {
    + safe: true
    + idempotent: true
    + body: не рекомендується
    ---
    Отримати ресурс
}

class "POST" as POST #ef9a9a {
    + safe: false
    + idempotent: false
    + body: обов'язково
    ---
    Створити ресурс або дія
}

class "PUT" as PUT #ffe082 {
    + safe: false
    + idempotent: true
    + body: обов'язково
    ---
    Замінити ресурс повністю
}

class "PATCH" as PATCH #ffcc80 {
    + safe: false
    + idempotent: false*
    + body: обов'язково
    ---
    Частково змінити ресурс
}

class "DELETE" as DELETE #f48fb1 {
    + safe: false
    + idempotent: true
    + body: ігнорується
    ---
    Видалити ресурс
}

class "HEAD" as HEAD #b3e5fc {
    + safe: true
    + idempotent: true
    + body: відсутнє
    ---
    GET без тіла відповіді
}

class "OPTIONS" as OPTIONS #e1bee7 {
    + safe: true
    + idempotent: true
    ---
    Дізнатись можливості
}

@enduml
МетодSafeIdempotentBodyТипове використання
GETОтримати список або ресурс
HEADПеревірити існування, metadata
OPTIONSCORS preflight, можливості
POSTСтворити ресурс, надіслати форму
PUTЗамінити ресурс повністю
PATCH❌*Часткове оновлення
DELETEВидалити ресурс
CONNECTПроксі-тунель
Чому PATCH не ідемпотентний?PATCH {"increment": 1} збільшить лічильник щоразу по-іншому. Але PATCH {"name": "Alice"} — фактично ідемпотентний. Ідемпотентність PATCH залежить від семантики операції, а не від самого методу.

Детальний розбір кожного методу


HTTP Status Codes: всі п'ять класів

Статус-код — це числова відповідь сервера, що однозначно вказує на результат обробки запиту. Перша цифра визначає клас відповіді, наступні дві — конкретний код.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

state "HTTP Response" as root {
    state "1xx\nInformational" as s1xx #e3f2fd
    state "2xx\nSuccess" as s2xx #e8f5e9
    state "3xx\nRedirection" as s3xx #fff9c4
    state "4xx\nClient Error" as s4xx #fce4ec
    state "5xx\nServer Error" as s5xx #f3e5f5

    [*] --> s1xx : Запит отримано,\nпродовжуємо
    [*] --> s2xx : Успіх
    [*] --> s3xx : Потрібне\nперенаправлення
    [*] --> s4xx : Помилка\nклієнта
    [*] --> s5xx : Помилка\nсервера
}

note right of s1xx
  100 Continue
  101 Switching Protocols
  103 Early Hints
end note

note right of s2xx
  200 OK
  201 Created
  204 No Content
  206 Partial Content
end note

note right of s3xx
  301 Moved Permanently
  302 Found
  304 Not Modified
  307 Temporary Redirect
  308 Permanent Redirect
end note

note right of s4xx
  400 Bad Request
  401 Unauthorized
  403 Forbidden
  404 Not Found
  409 Conflict
  422 Unprocessable Entity
  429 Too Many Requests
end note

note right of s5xx
  500 Internal Server Error
  502 Bad Gateway
  503 Service Unavailable
  504 Gateway Timeout
end note

@enduml

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 — Помилки клієнта

5xx — Помилки сервера

Ключова відмінність 4xx від 5xx: 4xx — помилка на стороні клієнта (неправильний запит), 5xx — помилка на стороні сервера (правильний запит, але сервер не зміг обробити). При 5xx клієнт може спробувати повторити запит пізніше.

HTTP Headers: детальний розбір

Заголовки — це метадані HTTP-повідомлення. Вони несуть інформацію про формат тіла, аутентифікацію, кешування, кодування, а також власні розширення застосунку. Заголовки розділяються на кілька категорій.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "HTTP Headers" #f5f5f5 {

    package "General Headers\n(запит і відповідь)" #e3f2fd {
        rectangle "Date" as date #bbdefb
        rectangle "Connection" as conn #bbdefb
        rectangle "Cache-Control" as cc #bbdefb
        rectangle "Transfer-Encoding" as te #bbdefb
    }

    package "Request Headers" #e8f5e9 {
        rectangle "Host" as host #a5d6a7
        rectangle "Accept / Accept-*" as accept #a5d6a7
        rectangle "Authorization" as auth #a5d6a7
        rectangle "User-Agent" as ua #a5d6a7
        rectangle "Referer" as ref #a5d6a7
        rectangle "If-None-Match" as inm #a5d6a7
    }

    package "Response Headers" #fff3e0 {
        rectangle "Content-Type" as ct #ffcc80
        rectangle "Content-Length" as cl #ffcc80
        rectangle "Location" as loc #ffcc80
        rectangle "Set-Cookie" as sc #ffcc80
        rectangle "WWW-Authenticate" as wa #ffcc80
        rectangle "ETag" as etag #ffcc80
    }

    package "Representation Headers\n(про тіло)" #fce4ec {
        rectangle "Content-Encoding" as ce #f48fb1
        rectangle "Content-Language" as clang #f48fb1
        rectangle "Content-Location" as cloc #f48fb1
    }
}

@enduml

Найважливіші заголовки запиту

Host
string (обов'язковий у HTTP/1.1)
Доменне ім'я та опціональний порт цільового сервера: Host: api.example.com або Host: localhost:5000. Обов'язковий починаючи з HTTP/1.1 — без нього сервер не знає, для якого віртуального хосту призначений запит. Єдиний обов'язковий заголовок у HTTP/1.1.
Accept
MIME-типи з q-factor
Вказує, які формати відповіді клієнт розуміє. Підтримує q-factor (пріоритет, від 0 до 1): Accept: text/html, application/json;q=0.9, */*;q=0.8 Сервер обирає найкращий доступний формат — це content negotiation.
Content-Type
MIME-тип
Формат тіла запиту (у POST, PUT, PATCH). Обов'язковий, якщо є тіло:
  • Content-Type: application/json — JSON
  • Content-Type: application/x-www-form-urlencoded — HTML-форма
  • Content-Type: multipart/form-data; boundary=----... — форма з файлами
Authorization
scheme credentials
Облікові дані для аутентифікації:
  • Authorization: Basic dXNlcjpwYXNz — Base64 (login:password)
  • Authorization: Bearer eyJhbGc... — JWT або OAuth токен
  • Authorization: Digest ... — Digest Auth
Accept-Encoding
алгоритми стиснення
Алгоритми стиснення, які підтримує клієнт: Accept-Encoding: gzip, deflate, br. Сервер може стиснути відповідь і вказати Content-Encoding: gzip. Браузери завжди підтримують gzip та brotli.
If-None-Match
ETag value
Умовний запит: надіслати відповідь тільки якщо ETag ресурсу відрізняється від вказаного. Якщо ресурс не змінився — сервер повертає 304 Not Modified без тіла. Це основа HTTP-кешування.
User-Agent
string
Ідентифікатор клієнта: браузера, бібліотеки або застосунку. Сервери іноді використовують для fingerprinting або видачі різного контенту. User-Agent: dotnet-httpclient/8.0.

Найважливіші заголовки відповіді

Content-Type
MIME-тип; charset
Формат тіла відповіді. Клієнт використовує для розбору:
  • Content-Type: application/json; charset=utf-8
  • Content-Type: text/html; charset=utf-8
  • Content-Type: image/webp
  • Content-Type: application/octet-stream — довільні бінарні дані
Content-Length
uint (байти)
Точний розмір тіла у байтах. Якщо відсутній — використовується Transfer-Encoding: chunked для потокової передачі. При Content-Length клієнт знає заздалегідь, скільки читати.
Cache-Control
директиви
Управління кешуванням. Найважливіші директиви:
  • no-cache — перевіряти актуальність перед використанням кешу
  • no-store — ніколи не кешувати (особисті дані)
  • max-age=3600 — кешувати 3600 секунд
  • public — можна кешувати у CDN та проксі
  • private — тільки у браузері користувача
ETag
string (версія ресурсу)
«Відбиток» (fingerprint) поточної версії ресурсу: ETag: "33a64df5". Клієнт зберігає і передає у наступному запиті як If-None-Match. Якщо збігається — 304 Not Modified. Якщо ні — нова версія ресурсу.
Location
URL
URL для редиректу (3xx) або URL нового ресурсу (201 Created). При 301/302 браузер автоматично переходить за цим URL.
Set-Cookie
cookie-string
Встановлює cookie у браузері клієнта. Один заголовок — один cookie. Формат: Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600; Path=/
WWW-Authenticate
scheme realm
Супроводжує 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. Розуміння їх взаємозв'язку є ключем до правильного вибору інструменту.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "System.Net.Http" #e3f2fd {

    class "HttpClient" as hc #bbdefb {
        + BaseAddress: Uri?
        + DefaultRequestHeaders: HttpRequestHeaders
        + Timeout: TimeSpan
        ---
        + GetAsync(url): Task<HttpResponseMessage>
        + PostAsync(url, content): Task<HttpResponseMessage>
        + PutAsync(url, content): Task<HttpResponseMessage>
        + PatchAsync(url, content): Task<HttpResponseMessage>
        + DeleteAsync(url): Task<HttpResponseMessage>
        + SendAsync(request): Task<HttpResponseMessage>
    }

    class "HttpRequestMessage" as hrq #c8e6c9 {
        + Method: HttpMethod
        + RequestUri: Uri?
        + Headers: HttpRequestHeaders
        + Content: HttpContent?
        + Version: Version
    }

    class "HttpResponseMessage" as hrs #fff9c4 {
        + StatusCode: HttpStatusCode
        + IsSuccessStatusCode: bool
        + Headers: HttpResponseHeaders
        + Content: HttpContent
        ---
        + EnsureSuccessStatusCode()
    }

    class "HttpMessageHandler" as hmh #f3e5f5 {
        # SendAsync(request, ct)
    }

    class "HttpClientHandler" as hch #e1bee7 {
        + AllowAutoRedirect: bool
        + UseCookies: bool
        + CookieContainer
        + ServerCertificateCustomValidation
        + AutomaticDecompression
    }

    class "DelegatingHandler" as dh #fce4ec {
        Перехоплення запитів:\nлогування, retry, auth
    }

    hc --> hmh : використовує
    hch --|> hmh
    dh --|> hmh
    dh --> hmh : InnerHandler
    hc ..> hrq : приймає
    hc ..> hrs : повертає
}

package "Microsoft.Extensions.Http" #e8f5e9 {
    class "IHttpClientFactory" as ihcf #a5d6a7 {
        + CreateClient(name): HttpClient
    }
    ihcf --> hc : створює
}

@enduml

Клас 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.
BaseAddress
Uri?
Базова адреса для всіх відносних запитів: client.BaseAddress = new Uri("https://api.example.com/v1/"). Дозволяє писати client.GetAsync("users") замість повної URL.
DefaultRequestHeaders
HttpRequestHeaders
Заголовки, що автоматично додаються до кожного запиту. Ідеально для Authorization, Accept, User-Agent.
Timeout
TimeSpan
Максимальний час очікування для запиту (за замовчуванням 100 секунд). При перевищенні — 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);

IHttpClientFactory: правильний спосіб в DI-застосунках

У застосунках на основі Microsoft.Extensions.DependencyInjection (ASP.NET Core, Worker Services) слід використовувати IHttpClientFactory замість прямого new HttpClient().

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Код застосунку" as app

component "IHttpClientFactory" as factory #a5d6a7 {
    component "Named Client Pool" as pool #c8e6c9
    component "HttpMessageHandler Pool\n(керування lifecycle)" as handlerPool #c8e6c9
    pool --> handlerPool
}

component "HttpClient" as hc #bbdefb
component "HttpMessageHandler" as hmh #e1bee7

app -> factory : CreateClient("MyApi")
factory -> pool : отримати або створити
pool -> hc : новий HttpClient\n(легкий об'єкт)
hc --> hmh : посилається
handlerPool --> hmh : керує lifecycle\n(2 хвилини за замовчуванням)

note right of handlerPool
    Handler живе 2 хвилини,
    потім замінюється новим.
    DNS-зміни підхоплюються.
    Немає socket exhaustion.
end note

@enduml
// 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.

JSONPlaceholder — безкоштовний публічний REST API для тестування. Підтримує GET, POST, PUT, PATCH, DELETE для ресурсів /posts, /users, /todos, /comments. Зміни не зберігаються на сервері — ідеально для навчання.

Що будуємо

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Консоль" as console
participant "Program.cs" as app #e3f2fd
participant "jsonplaceholder\n.typicode.com" as api #e8f5e9

console -> app : запуск
app -> api : GET /posts
api --> app : 200 OK + масив
app -> console : список перших 3

app -> api : GET /posts/9999
api --> app : 404 Not Found
app -> console : "не знайдено"

app -> api : POST /posts
api --> app : 201 Created
app -> console : Новий ID = 101

app -> api : PUT /posts/1
api --> app : 200 OK

app -> api : PATCH /posts/1
api --> app : 200 OK

app -> api : DELETE /posts/1
api --> app : 200 OK

@enduml

Кроки

Створюємо проєкт

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);

Вивід

dotnet run — PostsExplorer
════════════════════════════════════════════════
Posts Explorer — HTTP Demo
════════════════════════════════════════════════
▶ 1. GET /posts — список постів
HTTP 200 OK ← GET /posts
Отримано постів: 100. Перші 3:
[ 1] sunt aut facere repellat provident occaecati ex
[ 2] qui est esse
[ 3] ea molestias quasi exercitationem repellat qui ip
▶ 2. GET /posts/1 — один ресурс
HTTP 200 OK ← GET /posts/1
Title: sunt aut facere repellat provident occaecati...
Body: quia et suscipit suscipit recusandae consequuntu
▶ 3. GET /posts/9999 — обробка 404
HTTP 404 NotFound ← GET /posts/9999
→ Ресурс не знайдено. 404 оброблено gracefully.
▶ 4. POST /posts — створити пост
HTTP 201 Created ← POST /posts
Location: /posts/101
Новий ID: 101
▶ 5. PUT /posts/1 — повна заміна
HTTP 200 OK ← PUT /posts/1
Title: Повністю замінений заголовок
▶ 6. PATCH /posts/1 — оновити лише title
HTTP 200 OK ← PATCH /posts/1
Title: Оновлений через PATCH
Body залишився: quia et suscipit suscipit recusandae c
▶ 7. DELETE /posts/1 — видалити
HTTP 200 OK ← DELETE /posts/1
✓ Видалено
════════════════════════════════════════════════
Готово! Всі HTTP-методи виконано.
════════════════════════════════════════════════

Що ви щойно побудували

HTTP-методи

  • GET — безпечний, ідемпотентний, без тіла
  • POST201 Created + Location header
  • PUT — ідемпотентний, повна заміна всіх полів
  • PATCH — лише поля, що змінились
  • DELETE — ідемпотентний

Статус-коди

  • 200 OK — успішна операція
  • 201 Created — ресурс створено
  • 404 Not Found — обробка без виключень
  • IsSuccessStatusCode vs EnsureSuccessStatusCode()

HttpClient у C#

  • Один екземпляр для всіх запитів
  • PostAsJsonAsync / GetFromJsonAsync — серіалізація автоматична
  • JsonContent.Create — для PATCH і кастомних запитів
  • Headers.Location — читання заголовків відповіді
Спробуйте: змініть posts на users, todos або comments — JSONPlaceholder підтримує всі ці ресурси. Спробуйте навмисно надіслати некоректні дані і подивіться, яку відповідь повертає сервер.

Практика та закріплення

Теоретичний розбір HTTP легко створює оманливе відчуття зрозумілості — поки не спробуєш написати реальний клієнт. Наведені нижче завдання побудовані за принципом поступового ускладнення: від базового розуміння структури повідомлень до проектування власного HTTP-клієнта з підтримкою аутентифікації та повторних запитів.

Рівень 1. Базове розуміння

  1. Напишіть власними словами різницю між ідемпотентним та безпечним HTTP-методом. Наведіть по два приклади для кожної категорії.
  2. Яку HTTP-відповідь поверне сервер у кожному з цих сценаріїв? Обґрунтуйте вибір статус-коду:
    • Клієнт запитує ресурс, який не існує
    • Клієнт успішно створив новий об'єкт
    • Клієнт надіслав JSON з пропущеним обов'язковим полем
    • Клієнт намагається видалити об'єкт, що належить іншому користувачу
    • Сервер впав через непередбачений виняток у коді
  3. Яка різниця між заголовками Authorization: Basic ... та Authorization: Bearer ...? В яких сценаріях застосовується кожен?

Рівень 2. Робота з HttpClient

  1. Напишіть консольний застосунок, що:
    • Отримує список постів з jsonplaceholder.typicode.com/posts
    • Фільтрує пости з userId = 1
    • Виводить Id та перші 50 символів Title кожного поста
    • Виводить загальну кількість знайдених постів
  2. Реалізуйте HEAD-запит для перевірки існування ресурсу без завантаження тіла. Порівняйте час виконання HEAD та GET для одного ресурсу за допомогою Stopwatch.
  3. Напишіть метод DownloadWithProgressAsync(string url, string filePath), що:
    • Завантажує файл за URL з підтримкою великих файлів (потокове читання через ReadAsStreamAsync)
    • Виводить прогрес у відсотках (якщо сервер повертає Content-Length)
    • Не завантажує весь файл у RAM одночасно

Рівень 3. Архітектурне мислення

  1. Реалізуйте CircuitBreakerHandler : DelegatingHandler, що:
    • Відстежує кількість поспіль невдалих запитів (5xx або TaskCanceledException)
    • При 5 невдачах поспіль переходить у стан «Відкритий» (Open) — відхиляє нові запити без надсилання
    • Через 30 секунд переходить у «Напіввідкритий» (Half-Open) — пропускає один тестовий запит
    • При успішному тестовому запиті повертається у «Закритий» (Closed)
    • Використайте PlantUML state diagram для документування станів
  2. Спроектуйте типізований GitHubApiClient із підтримкою:
    • Rate limiting (заголовки X-RateLimit-Remaining, X-RateLimit-Reset)
    • Автоматичного очікування при вичерпанні ліміту
    • Pagination через Link header
    • Bearer token аутентифікації через IOptions<GitHubOptions>
Правильна відповідь у HTTP-програмуванні завжди починається зі структури запиту: перевірте метод, URL, заголовки та тіло — і лише потім шукайте проблему у коді клієнта.

Контрольні питання

Нижче подано набір запитань для самоперевірки. Якщо на будь-яке з них важко відповісти без повторного читання — це нормальний сигнал повернутись до відповідного розділу.

  1. Чим відрізняється HTTP від TCP? Чому HTTP не може існувати без TCP (або QUIC)?
  2. Що таке stateless і як це фундаментально впливає на архітектуру вебзастосунків?
  3. Яку мінімальну кількість заголовків обов'язково містить валідний HTTP/1.1 запит?
  4. В чому відмінність між PUT та PATCH? Наведіть сценарій, де важливо вибрати правильно.
  5. Чому 401 Unauthorized — погана назва? Яка семантична різниця між 401 та 403?
  6. Що таке socket exhaustion у HttpClient і як IHttpClientFactory це вирішує?
  7. Навіщо потрібен DelegatingHandler? Наведіть три реальних сценарії його використання.
  8. Що відбудеться, якщо сервер поверне 301 Moved Permanently у відповідь на POST-запит? Що відбудеться при 308?
  9. Для чого використовується HEAD-метод? Наведіть два практичних прикладів.
  10. Що означає ETag і як він пов'язаний зі статус-кодом 304 Not Modified?
Якщо ви впевнено відповідаєте на всі 10 питань, у вас є міцна база для переходу до наступного розділу: cookies, сесії та аутентифікація в HTTP. Там ми розберемо, як вирішується проблема stateless протоколу у реальних застосунках.
Copyright © 2026