HTTP: методи, структура, заголовки
Зміст
HTTP (HyperText Transfer Protocol) — це протокол прикладного рівня, яким браузер, мобільний застосунок чи ваш автотест «розмовляють» із сервером. Він працює за схемою «запит — відповідь» (request — response) і не зберігає стану між запитами (stateless).
У HTTP/1.x повідомлення (message) — це звичайний текст, який людина читає очима. HTTP/2 і HTTP/3 кодують те саме у вигляді бінарних кадрів (frames), але логічна модель — метод, шлях, заголовки, тіло — залишається незмінною. Тому, розібравши структуру на прикладі HTTP/1.1, ви розумієте всі три версії.
Властивість stateless критична для розуміння: сервер за замовчуванням не памʼятає, що ви робили попереднім запитом. Усе, що потрібно для обробки, ви маєте передати в самому запиті — в адресі, заголовках або тілі. Саме тому автотести мусять самостійно дбати про автентифікацію, куки й стан: протокол за вас цього не робить.
Для AQA HTTP — не абстракція. Це рівень, на якому ви налаштовуєте API-фікстури, перехоплюєте (intercept) мережу у Playwright, шукаєте джерело флаку (flakiness) і відрізняєте баг застосунку від «моргання» мережі. Тож розберемо його по кістках.
Анатомія запиту й відповіді
Будь-яке HTTP-повідомлення — і запит, і відповідь — складається з трьох частин у сталому порядку:
- Стартовий рядок (start-line) — перший рядок.
- Заголовки (headers) — набір пар «назва: значення», по одній на рядок.
- Тіло (body) — необовʼязкові дані; відокремлене від заголовків одним порожнім рядком.
Порожній рядок між заголовками й тілом — не косметика, а частина синтаксису: саме він каже парсеру «заголовки скінчилися, далі йде тіло».
Запит
POST /api/login HTTP/1.1
Host: app.example.com
Content-Type: application/json
Accept: application/json
Content-Length: 46
{"email":"qa@example.com","password":"secret"}
Стартовий рядок запиту (request-line) має три елементи через пробіл: метод (POST), ціль запиту (request target, зазвичай шлях із query-рядком — /api/login) і версію протоколу (HTTP/1.1). Далі — заголовки, порожній рядок і тіло з даними форми логіну. Заголовок Content-Length вказує розмір тіла в байтах, щоб приймач знав, скільки читати.
Відповідь
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 51
Cache-Control: no-store
{"token":"eyJhbGciOiJIUzI1NiJ9...","expiresIn":3600}
Стартовий рядок відповіді (status-line) теж має три елементи: версію, код статусу (status code, 200) і пояснювальну фразу (reason phrase, OK). Фраза призначена для людини — клієнт орієнтується на числовий код, а фразу може ігнорувати.
Ця тричленна текстова структура — властивість HTTP/1.1. У HTTP/2 і HTTP/3 стартового рядка як окремого рядка немає взагалі: метод, шлях, схему, авторитет і код статусу передають як спеціальні псевдозаголовки (:method, :path, :scheme, :authority, :status), а пояснювальної фрази не існує. Логічна модель «метод/шлях/статус + заголовки + тіло» зберігається — змінюється лише кодування на дроті.
Коди статусів згруповані в пʼять класів за першою цифрою:
| Клас | Значення | Приклади |
|---|---|---|
| 1xx | Інформаційні, проміжні | 100 Continue, 101 Switching Protocols |
| 2xx | Успіх | 200 OK, 201 Created, 204 No Content |
| 3xx | Перенаправлення | 301 Moved Permanently, 304 Not Modified |
| 4xx | Помилка клієнта | 400 Bad Request, 401 Unauthorized, 404 Not Found |
| 5xx | Помилка сервера | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable |
Для автотестів цей поділ — перша лінія діагностики. 4xx означає «клієнт (тобто ваш запит) зробив щось не так»: не той шлях, відсутній чи хибний токен, невалідне тіло. 5xx — «сервер упав на коректному запиті», тобто ймовірний баг застосунку, а не тесту. Плутати ці класи — значить заводити баги на власні тести або, навпаки, списувати падіння продукту на «нестабільність».
Тричленну структуру інструменти показують буквально: у DevTools вкладка Network розкладає запит на Headers / Payload / Response, а curl -v виводить рядки запиту з префіксом > і рядки відповіді з <.
Основні методи та їх семантика
Метод (method, іноді кажуть «дієслово», verb) повідомляє, що ви хочете зробити з ресурсом за вказаною адресою.
| Метод | Призначення | Safe | Ідемпотентний | Тіло у запиті |
|---|---|---|---|---|
| GET | Отримати ресурс | так | так | зазвичай ні |
| HEAD | Як GET, але лише заголовки | так | так | ні |
| OPTIONS | Дізнатися можливості/дозволені методи | так | так | зазвичай ні |
| POST | Створити / надіслати дані на обробку | ні | ні | так |
| PUT | Повністю замінити (або створити) ресурс | ні | так | так |
| PATCH | Частково змінити ресурс | ні | як правило, ні | так |
| DELETE | Видалити ресурс | ні | так | необовʼязково |
| TRACE | Діагностика: ехо запиту | так | так | ні |
TRACE і CONNECT на практиці майже не трапляються: CONNECT застосовують проксі для тунелювання (наприклад, під HTTPS), а TRACE часто взагалі вимкнений на серверах з міркувань безпеки. Решту методів розберемо детальніше — саме навколо них будуються питання на співбесідах і реальні рішення в автоматизації.
Safe-методи
Safe-метод — це метод, який за семантикою лише читає й не змінює стан ресурсу на сервері. До безпечних належать GET, HEAD, OPTIONS і TRACE. «Безпечний» тут не про захист даних, а про відсутність побічних ефектів: клієнт, проксі чи пошуковий бот можуть викликати такий запит скільки завгодно разів і нічого не зіпсують.
Слово «за семантикою» важливе. HTTP описує наміри, а не гарантує поведінку. Ніщо фізично не заважає розробнику зробити GET /users/42/delete, який видаляє користувача. Але це порушення контракту протоколу: пошуковий робот або префетч у браузері спокійно «клікне» таке посилання й зруйнує ваші дані. Для AQA це прямий сигнал: якщо GET-ендпоінт мутує стан — це баг, і його варто зафіксувати.
Ідемпотентність
Ідемпотентний метод — це метод, у якого стан сервера після одного виклику й після N однакових викликів поспіль однаковий. Ідемпотентність стосується стану сервера, а не тіла відповіді (тіло чи заголовки можуть відрізнятися).
Ідемпотентні: GET, HEAD, OPTIONS, TRACE, PUT, DELETE. Неідемпотентні: POST і, як правило, PATCH.
Розберемо неочевидні випадки:
PUTідемпотентний, бо замінює ресурс цілком. Надішліть той самийPUT /users/42десять разів — ресурс залишиться в тому самому стані, що й після першого.DELETEідемпотентний попри те, що другий виклик поверне інший код (перший —200/204, другий —404). Стан сервера незмінний: ресурсу немає й немає. Ідемпотентність — про стан, не про код відповіді.POSTнеідемпотентний: два однаковіPOST /ordersстворять два замовлення. Саме тому подвійний клік по кнопці «Оплатити» без захисту породжує дублі.PATCHу загальному випадку неідемпотентний: наприклад, патч «додай +1 до лічильника» щоразу дає новий результат. Хоча конкретний патч («встановиstatus = "done"») може бути ідемпотентним, специфікація не вимагає цього від методу.
Практична цінність для автотестів велика. Ідемпотентні запити можна безпечно повторювати за таймауту чи мережевого збою — тому клієнтські бібліотеки часто автоматично ретраять (retry) саме GET/PUT/DELETE, але не POST. Якщо ваш тест флакне через мережу, знати ідемпотентність методу — значить розуміти, чи безпечний ретрай, чи він зіпсує дані.
GET vs POST
Різниця не в тому, що «GET читає, а POST пише» (це спрощення), а в цілому наборі властивостей:
| Критерій | GET | POST |
|---|---|---|
| Семантика | отримати ресурс | надіслати дані на обробку |
| Safe | так | ні |
| Ідемпотентний | так | ні |
| Де передаються дані | у query-рядку URL | у тілі запиту |
| Кешується | так (за замовчуванням) | як правило, ні |
| Осідає в історії/логах | так (у URL) | тіло — ні |
| Повторна відправка форми (F5) | без попередження | браузер попереджає |
Ключовий висновок для тестування безпеки: параметри GET осідають в URL, а отже — в історії браузера, логах сервера, реферерах і закладках. Тому паролі, токени й персональні дані передають тілом POST, а не query-рядком GET. Токен у GET-параметрі — знахідка для security-репорту.
PUT vs PATCH
Обидва змінюють наявний ресурс, але по-різному:
PUTзаміняє ресурс повністю. Ви надсилаєте цілісне представлення, і сервер робить його новим станом. Поля, які ви не передали, зазвичай трактуються як «їх треба скинути» — залежно від реалізації.PATCHзастосовує часткову зміну. Ви надсилаєте лише дельту.
PUT /api/users/42 HTTP/1.1
Content-Type: application/json
{"name":"Ivan","email":"ivan@example.com","role":"admin"}
PATCH /api/users/42 HTTP/1.1
Content-Type: application/json
{"role":"admin"}
У прикладі з PUT ви зобовʼязані передати всі поля, інакше ризикуєте затерти name й email. У PATCH ви чіпаєте лише role. Звідси й різниця в ідемпотентності: PUT ідемпотентний за визначенням, PATCH — не обовʼязково.
Формат тіла PATCH не задано одним стандартом. Існують формалізовані медіа-типи — application/json-patch+json (RFC 6902, список операцій на кшталт «replace шлях X значенням Y») і application/merge-patch+json (RFC 7396, «накласти цей частковий обʼєкт зверху») — але на практиці багато API просто приймають частковий JSON у власному форматі. Перед написанням тестів дивіться контракт конкретного API, а не припускайте.
Чи можна тіло в GET
Технічно — так, синтаксис HTTP не забороняє тіло в жодному методі. Семантично — ні: специфікація каже, що тіло в GET не має загальновизначеного значення, не може змінити зміст чи ціль запиту, і клієнту рекомендовано його не генерувати. На практиці це означає непередбачувану поведінку — одні сервери й проксі проігнорують тіло, інші відкинуть запит (зокрема через ризик request smuggling), треті оброблять.
Тому в реальному житті тіло в GET не використовують. Відомий виняток — Elasticsearch, який дозволяє GET із тілом для пошукових запитів (і паралельно підтримує той самий запит через POST саме через цю невизначеність). Якщо потрібно передати складну структуру для «читання» — використовуйте POST, а не тіло в GET.
Метод OPTIONS
OPTIONS запитує, які можливості доступні для ресурсу — насамперед які HTTP-методи він приймає. Сервер відповідає заголовком Allow:
OPTIONS /api/users/42 HTTP/1.1
Host: app.example.com
HTTP/1.1 204 No Content
Allow: GET, HEAD, PUT, PATCH, DELETE, OPTIONS
Але найважливіша для фронтенд-тестування роль OPTIONS — CORS preflight. Коли браузер збирається зробити «непростий» крос-доменний запит (наприклад, PATCH із заголовком Content-Type: application/json на інший домен), він спершу автоматично надсилає OPTIONS-розвідку із заголовками Access-Control-Request-Method і Access-Control-Request-Headers. Тільки якщо сервер відповість дозвільними Access-Control-Allow-*, браузер відправить справжній запит. Зверніть увагу: application/json не входить до «безпечного» (safelisted) переліку типів, тому запит із таким тілом завжди тягне за собою preflight — на відміну від application/x-www-form-urlencoded, multipart/form-data чи text/plain.
Для AQA це часте джерело плутанини: у Network ви бачите «зайвий» OPTIONS перед кожним POST/PATCH, а тест, що «падає на CORS», насправді падає на preflight. Розуміння цього економить години на хибних діагнозах.
Метод HEAD
HEAD ідентичний GET, але сервер повертає лише заголовки, без тіла. Заголовки при цьому такі самі, якими були б для GET (зокрема Content-Length, Content-Type, Last-Modified).
HEAD /downloads/report.pdf HTTP/1.1
Host: files.example.com
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 1048576
Навіщо це AQA? Щоб дешево перевірити факти без завантаження вмісту: чи існує файл (200 vs 404), який його розмір і тип, чи змінився він від певного часу. У health-check і смоук-тестах HEAD дає перевірку доступності без витрат на трафік тіла.
Заголовки з прикладами
Заголовки (headers) — це метадані запиту/відповіді. Назви заголовків нечутливі до регістру (Content-Type і content-type — те саме); чутливість значення залежить від конкретного заголовка. Розглянемо найважливіші для тестувальника.
Content-Type
Content-Type описує медіа-тип (media type, MIME type) тіла, яке зараз передається, — відповідає на запитання «що саме лежить у body цього повідомлення».
Content-Type: application/json
Content-Type: application/x-www-form-urlencoded
Content-Type: multipart/form-data; boundary=----abc123
Content-Type: text/html; charset=utf-8
Це двосторонній заголовок: у запиті він описує тіло, яке шле клієнт; у відповіді — тіло, яке шле сервер. Помилковий Content-Type — класична причина багів: якщо клієнт шле JSON, але з Content-Type: text/plain, сервер може не розпарсити тіло й повернути 400. У тестах API це поширений недогляд, коли тіло формують вручну, а заголовок забувають.
Accept
Accept — це побажання клієнта: у якому форматі він хоче отримати відповідь. Сервер може врахувати його (механізм зветься content negotiation) і віддати відповідний формат.
Accept: application/json
Accept: text/html,application/xhtml+xml;q=0.9,*/*;q=0.8
Значення q — це вагові коефіцієнти переваги (quality value) від 0 до 1: чим вище, тим бажаніший формат; за відсутності q перевага дорівнює 1.
Різниця Accept vs Content-Type
Їх легко сплутати, бо обидва оперують медіа-типами. Різниця — у напрямку й моменті:
| Content-Type | Accept | |
|---|---|---|
| Що описує | що є в тілі зараз | що клієнт хоче отримати |
| Напрямок | опис наявного тіла | побажання щодо майбутньої відповіді |
| Де є | у запиті й у відповіді | переважно у запиті |
| Приклад | «я надсилаю JSON» | «надішли мені JSON» |
Проста мнемоніка: Content-Type — «ось що я вклав у конверт», Accept — «ось у якому форматі я хочу відповідь». У запиті вони можуть відрізнятися: клієнт шле форму (Content-Type: application/x-www-form-urlencoded), а відповідь хоче у JSON (Accept: application/json). (Формально специфікація дозволяє Accept і у відповіді — щоб сервер підказав, які типи він приймає, — але на практиці це рідкість.)
Authorization
Authorization несе облікові дані для доступу до захищеного ресурсу. Найпоширеніші схеми:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0...
Authorization: Basic dXNlcjpwYXNz
- Bearer — токен (часто JWT). Хто володіє токеном, той має доступ (bearer = «предʼявник»).
- Basic — це
base64("логін:пароль"). У прикладіdXNlcjpwYXNzдекодується уuser:pass. Важливо: base64 — це кодування, а не шифрування, воно тривіально розкривається. Тому Basic-auth без HTTPS передає пароль фактично відкритим текстом.
Якщо сервер вимагає автентифікації, він відповідає 401 Unauthorized разом із заголовком WWW-Authenticate, який підказує, яку схему використати.
Для AQA Authorization — найчастіший інгредієнт API-фікстур: замість щоразу логінитися через UI (повільно й крихко), тест один раз отримує токен і додає його в заголовок наступних запитів. Це і швидше, і стабільніше.
User-Agent
User-Agent ідентифікує клієнтське ПЗ — браузер, його версію, рушій, іноді ОС. Приклади:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
User-Agent: curl/8.4.0
User-Agent: PostmanRuntime/7.36.0
Сервери іноді змінюють поведінку залежно від User-Agent (мобільна vs десктопна версія, блокування ботів). Для тестувальника це двобічний меч. По-перше, автотест із дефолтним User-Agent бібліотеки (curl/..., PostmanRuntime/...) може отримати іншу відповідь, ніж реальний браузер, і дати хибний результат. По-друге, підміняючи User-Agent, можна тестувати різні гілки поведінки (наприклад, «мобільний» рендер) без реального пристрою.
Query-параметри
Query-рядок — це частина URL після ?, набір пар ключ=значення, зʼєднаних &:
https://app.example.com/search?q=login&page=2&sort=desc
Тут три параметри: q=login, page=2, sort=desc. Значення зі спецсимволами (пробіли, кирилиця, &, =) мають бути percent-encoded (URL-кодування): пробіл стає %20 (або + у формах), & всередині значення — %26 тощо. Пропущене кодування — типова причина, коли параметр «губиться» або ламає запит.
Query vs тіло запиту
І query, і тіло передають дані серверу — де межа?
| Query-параметри | Тіло запиту | |
|---|---|---|
| Розташування | в URL, після ? | після заголовків |
| З якими методами | будь-якими, типово GET | POST/PUT/PATCH |
| Видимість | видно в URL, історії, логах | не в URL |
| Обмеження довжини | так, URL не безмежний | практично значно більше |
| Типове призначення | фільтри, пошук, пагінація, сортування | створення/оновлення сутностей |
| Структура даних | плоскі пари ключ-значення | довільна (JSON, форма, файли) |
Практичне правило: query — для параметрів читання (як відфільтрувати, відсортувати, яку сторінку), тіло — для даних, що змінюють стан або мають складну структуру. Секрети — завжди в тіло, ніколи в query.
Специфікація HTTP не задає жорсткого ліміту довжини URL, але сервери й проксі накладають власні — типово порядку 8 КБ на request-line (наприклад, дефолтний LimitRequestLine в Apache — 8190 байтів, а буфер запиту в nginx — 8 КБ, і request-line не може перевищити один такий буфер, інакше повертається 414). Тому великі набори даних не варто пхати в query.
multipart/form-data
Коли форма містить файл, звичайного application/x-www-form-urlencoded замало — бінарні дані так надійно не передати. Для цього існує multipart/form-data: тіло розбивається на частини (parts), кожна зі своїми міні-заголовками, а частини розділяє унікальний рядок-розділювач (boundary), оголошений у Content-Type.
POST /api/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="title"
Quarterly report
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf
%PDF-1.4 ... (бінарний вміст файлу) ...
------WebKitFormBoundaryABC123--
Кожна частина починається роздільником — це два дефіси плюс значення boundary (у цьому прикладі саме значення вже містить дефіси, тому на дроті видно ------WebKitFormBoundaryABC123), а весь блок завершується тим самим роздільником із ще двома дефісами в кінці (------WebKitFormBoundaryABC123--). Частина з файлом додатково має filename і власний Content-Type. Boundary мусить бути таким, що гарантовано не трапиться у вмісті даних.
Для автотестів завантаження файлів — часте джерело плутанини: якщо формувати multipart вручну (кривий curl), легко зіпсувати boundary чи заголовки частин і отримати 400. Тому у Playwright і HTTP-клієнтах користуйтеся вбудованими засобами (передача файлу обʼєктом), які самі складають коректний multipart.
Версії протоколу: HTTP/1.1 vs 2 vs 3
Логіка (методи, заголовки, коди статусів) у всіх трьох версіях спільна — змінюється спосіб передачі байтів по мережі.
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Формат | текстовий | бінарні кадри (frames) | бінарні кадри |
| Транспорт | TCP | TCP | QUIC (поверх UDP) |
| Одночасні запити на зʼєднанні | по черзі (1 за раз) | мультиплексування | мультиплексування |
| Стиснення заголовків | немає | HPACK | QPACK |
| Head-of-line blocking | так (на рівні запитів) | усунено на рівні HTTP, лишається на рівні TCP | усунено й на транспорті |
- HTTP/1.1 — текстовий і послідовний: у межах одного зʼєднання запити йдуть по черзі. Якщо перший «застряг», решта чекає (head-of-line blocking). Браузери обходили це, відкриваючи кілька паралельних зʼєднань до домену.
- HTTP/2 перейшов на бінарні кадри й мультиплексування: багато запитів і відповідей течуть паралельно одним TCP-зʼєднанням. Додав стиснення заголовків (HPACK), що суттєво зменшує накладні витрати, коли заголовки повторюються від запиту до запиту.
- HTTP/3 переносить усе на QUIC поверх UDP. Це усуває head-of-line blocking уже на транспортному рівні (втрата одного пакета не гальмує решту потоків) і пришвидшує встановлення зʼєднання, обʼєднуючи рукостискання TLS із транспортним.
Server push, що зʼявився у HTTP/2, дозволяв серверу надсилати ресурси до того, як клієнт їх попросить, але великі браузери згорнули його підтримку (Chrome і Edge вимкнули його за замовчуванням із версії 106, а сам механізм задепрекейчено в оновленій специфікації HTTP/2) — не покладайтеся на нього в тестах.
Для більшості функціональних автотестів версія протоколу прозора — ви працюєте із семантикою, а не з кадрами. Але вона впливає на продуктивність і діагностику: у DevTools колонка Protocol показує h2/h3/http/1.1, і це буває корисним при розслідуванні повільних чи нестабільних відповідей.
Keep-alive
Keep-alive — це перевикористання одного TCP-зʼєднання під кілька запитів поспіль замість того, щоб відкривати й закривати зʼєднання щоразу. Встановлення TCP- (і TLS-) зʼєднання коштує часу, тому тримати його відкритим — суттєвий виграш.
У HTTP/1.0 зʼєднання за замовчуванням закривалося, і keep-alive вмикали явно заголовком Connection: keep-alive. У HTTP/1.1 постійні зʼєднання (persistent connections) стали типовими — зʼєднання лишається відкритим, доки одна зі сторін не надішле Connection: close.
GET /api/health HTTP/1.1
Host: app.example.com
Connection: keep-alive
У HTTP/2 і HTTP/3 сама ідея «одне зʼєднання — багато запитів» вбудована в протокол через мультиплексування, тому заголовок Connection там не застосовується (у HTTP/2 він навіть заборонений як connection-specific / hop-by-hop-заголовок).
Практичний наслідок для навантажувальних і API-тестів: HTTP-клієнт з увімкненим keep-alive та пулом зʼєднань дає радикально інші (реалістичніші й швидші) цифри, ніж клієнт, що відкриває нове зʼєднання на кожен запит. Якщо ваш перформанс-тест показує підозріло високу затримку — перевірте, чи не створюєте ви зʼєднання заново щоразу.
Що таке HTTP і що означає, що він «не зберігає стану»?
HTTP (HyperText Transfer Protocol) — протокол прикладного рівня, за яким клієнт (браузер, мобільний застосунок чи автотест) спілкується із сервером за схемою «запит — відповідь». Ключова властивість — stateless: сервер за замовчуванням не памʼятає, що клієнт робив попереднім запитом, тож усе потрібне для обробки треба передати в самому запиті — в адресі, заголовках або тілі. Для автотестів це має прямий наслідок: протокол не подбає за вас про автентифікацію, куки й стан — тест мусить сам отримати токен чи куку і додавати їх до кожного наступного запиту.
З яких частин складається HTTP-повідомлення?
І запит, і відповідь мають три частини в сталому порядку: стартовий рядок, заголовки (пари «назва: значення», по одній на рядок) і необовʼязкове тіло. Тіло відокремлюється від заголовків одним порожнім рядком, і це не косметика, а частина синтаксису: саме порожній рядок каже парсеру, що заголовки скінчилися. Стартовий рядок запиту (request-line) містить метод, ціль запиту (зазвичай шлях із query-рядком) і версію протоколу; стартовий рядок відповіді (status-line) — версію, код статусу й пояснювальну фразу, на яку клієнт може не зважати. Це текстова структура HTTP/1.1; у HTTP/2 і HTTP/3 окремого стартового рядка немає — метод, шлях і статус передаються псевдозаголовками на кшталт :method, :path, :status, але логічна модель та сама. В інструментах це видно буквально: DevTools розкладає запит на Headers / Payload / Response, а curl -v префіксує рядки запиту символом >, а відповіді — <.
Які класи кодів статусів існують і як їх читати?
Пʼять класів за першою цифрою: 1xx — інформаційні, 2xx — успіх (200 OK, 201 Created, 204 No Content), 3xx — перенаправлення (301 Moved Permanently, 304 Not Modified), 4xx — помилка клієнта (400, 401, 404), 5xx — помилка сервера (500, 502, 503). Для тестувальника цей поділ — перша лінія діагностики: 4xx означає, що щось не так із самим запитом (не той шлях, відсутній чи хибний токен, невалідне тіло), а 5xx — що сервер упав на коректному запиті, тобто це ймовірний баг застосунку. Плутати класи — значить заводити баги на власні тести або, навпаки, списувати реальні падіння продукту на «нестабільність». Окремий нюанс: пояснювальна фраза (OK, Not Found) призначена для людини — клієнт орієнтується лише на числовий код, а в HTTP/2 і HTTP/3 фрази взагалі не існує.
Чим GET відрізняється від POST?
Різниця не зводиться до «GET читає, POST пише» — це спрощення, а насправді відрізняється цілий набір властивостей. GET безпечний (safe) та ідемпотентний, передає дані в query-рядку URL і за замовчуванням кешується; POST не safe і не ідемпотентний, дані несе в тілі, як правило не кешується, а при повторній відправці форми (F5) браузер попереджає користувача. Найважливіший практичний наслідок — безпековий: параметри GET осідають в URL, а отже в історії браузера, логах сервера, реферерах і закладках. Тому паролі, токени й персональні дані передають тілом POST, а токен у GET-параметрі — готова знахідка для security-репорту.
Навіщо потрібен метод HEAD?
HEAD ідентичний GET, але сервер повертає лише заголовки, без тіла — причому заголовки такі самі, якими були б для GET, зокрема Content-Length, Content-Type, Last-Modified. Це дає змогу дешево перевірити факти без завантаження вмісту: чи існує файл (200 проти 404), який його розмір і тип, чи змінився він від певного часу. Для AQA це зручний інструмент у health-check і смоук-тестах: перевірка доступності ресурсу без витрат на трафік тіла — наприклад, що звіт на 1 МБ існує, не викачуючи цей мегабайт.
У чому різниця між заголовками Content-Type і Accept?
Обидва оперують медіа-типами (MIME types), але напрямок різний: Content-Type описує тіло, яке передається зараз («ось що я вклав у конверт»), а Accept — побажання клієнта щодо формату майбутньої відповіді («надішли мені JSON»). Content-Type двосторонній — є і в запиті, і у відповіді; Accept живе переважно в запиті, і сервер може врахувати його через механізм узгодження вмісту (content negotiation), а значення q задають вагу переваги від 0 до 1. В одному запиті вони можуть відрізнятися: клієнт шле форму з Content-Type: application/x-www-form-urlencoded, а відповідь хоче отримати як Accept: application/json. Практична пастка для API-тестів: якщо тіло формують вручну і шлють JSON із Content-Type: text/plain, сервер може не розпарсити тіло й повернути 400 — типовий недогляд, коли заголовок просто забули.
Що таке safe-методи і чому GET, який змінює дані, — це баг?
Safe-метод за семантикою лише читає й не змінює стан ресурсу на сервері: це GET, HEAD, OPTIONS і TRACE. «Безпечний» тут не про захист даних, а про відсутність побічних ефектів — клієнт, проксі чи пошуковий бот можуть викликати такий запит скільки завгодно разів і нічого не зіпсують. Ключове слово — «за семантикою»: HTTP описує наміри, а не гарантує поведінку, і ніщо фізично не заважає розробнику зробити GET /users/42/delete, який реально видаляє користувача. Але це порушення контракту протоколу: пошуковий робот або префетч у браузері спокійно «клікне» таке посилання й зруйнує дані. Тому для AQA GET-ендпоінт, що мутує стан, — не «особливість реалізації», а баг, який варто зафіксувати.
Що таке ідемпотентність методу і які методи ідемпотентні?
Ідемпотентний метод — той, у якого стан сервера після одного виклику й після N однакових викликів поспіль однаковий. Важливо, що йдеться саме про стан сервера, а не про відповідь — тіло чи код можуть відрізнятися між викликами. Ідемпотентні: GET, HEAD, OPTIONS, TRACE, PUT, DELETE; неідемпотентні: POST і, як правило, PATCH (патч «додай +1 до лічильника» щоразу дає новий результат, хоча конкретний патч типу «встанови статус done» може бути ідемпотентним). Класика неідемпотентності POST: два однакові POST /orders створять два замовлення — саме тому подвійний клік по «Оплатити» без захисту породжує дублі. Практична цінність для автотестів: ідемпотентні запити можна безпечно повторювати за таймауту чи мережевого збою, тому клієнтські бібліотеки часто автоматично ретраять GET/PUT/DELETE, але не POST — і якщо тест флакне через мережу, знання ідемпотентності підказує, чи безпечний ретрай, чи він зіпсує дані.
Чому DELETE вважається ідемпотентним, якщо повторний виклик повертає 404?
Тому що ідемпотентність — про стан сервера, а не про код відповіді. Перший DELETE видаляє ресурс і повертає 200 або 204, другий — 404, але стан сервера в обох випадках однаковий: ресурсу немає й немає. Це улюблене питання-пастка на співбесідах: кандидати часто плутають «однаковий результат» з «однаковою відповіддю». Для автотестів звідси практичний висновок: асертити, що повторний DELETE поверне той самий код, — помилка дизайну тесту; перевіряти треба стан (ресурс відсутній), а різні коди першого й другого викликів — очікувана поведінка.
Яка різниця між PUT і PATCH?
PUT замінює ресурс повністю: ви надсилаєте цілісне представлення, і сервер робить його новим станом, а поля, які ви не передали, зазвичай трактуються як «їх треба скинути» — залежно від реалізації. PATCH застосовує часткову зміну: ви надсилаєте лише дельту, наприклад тільки поле role, не чіпаючи решту. Звідси й різниця в ідемпотентності: PUT ідемпотентний за визначенням (та сама повна заміна дає той самий стан), а PATCH — не обовʼязково. Нюанс для сеньйора: формат тіла PATCH не заданий одним стандартом — існують application/json-patch+json (RFC 6902, список операцій) і application/merge-patch+json (RFC 7396, «накласти частковий обʼєкт зверху»), але багато API просто приймають частковий JSON у власному форматі. Тому перед написанням тестів на PATCH дивіться контракт конкретного API, а не припускайте; а тестуючи PUT, перевіряйте кейс «передав не всі поля» — чи не затерлися name та email.
Як працює заголовок Authorization і чим Bearer відрізняється від Basic?
Authorization несе облікові дані для доступу до захищеного ресурсу. Дві найпоширеніші схеми: Bearer — токен (часто JWT), де доступ має будь-який предʼявник токена, і Basic — це base64("логін:пароль"). Критичний нюанс: base64 — це кодування, а не шифрування, воно тривіально розкривається, тому Basic без HTTPS передає пароль фактично відкритим текстом. Якщо сервер вимагає автентифікації, він відповідає 401 Unauthorized разом із заголовком WWW-Authenticate, що підказує потрібну схему. Для AQA Authorization — найчастіший інгредієнт API-фікстур: замість повільного й крихкого логіну через UI тест один раз отримує токен і додає його в заголовок наступних запитів — швидше і стабільніше.
Коли дані передавати query-параметрами, а коли тілом запиту?
Практичне правило: query — для параметрів читання (фільтри, пошук, пагінація, сортування), тіло — для даних, що змінюють стан, або зі складною структурою (JSON, форми, файли). Query видно в URL, історії та логах, тому секрети — завжди в тіло, ніколи в query. Query — це плоскі пари «ключ=значення» після ?, зʼєднані &, і значення зі спецсимволами мають бути percent-encoded: пробіл стає %20 (або + у формах), & усередині значення — %26; пропущене кодування — типова причина, коли параметр «губиться» або ламає запит. Ще одне обмеження — довжина: специфікація ліміту URL не задає, але сервери й проксі накладають власні, типово порядку 8 КБ на request-line (дефолтний LimitRequestLine в Apache — 8190 байтів; у nginx request-line не може перевищити 8-кілобайтний буфер, інакше 414). Тому великі набори даних у query не пхають.
Чому автотест може отримати іншу відповідь, ніж реальний браузер, на той самий запит?
Одна з типових причин — заголовок User-Agent, який ідентифікує клієнтське ПЗ. Сервери іноді змінюють поведінку залежно від нього: віддають мобільну чи десктопну версію, блокують ботів. Автотест із дефолтним User-Agent бібліотеки (curl/8.4.0, PostmanRuntime/...) може отримати іншу відповідь, ніж Chrome, і дати хибний результат — тест зелений або червоний не тому, що застосунок так працює для користувача. Це двобічний меч: підміняючи User-Agent, можна навпаки цілеспрямовано тестувати різні гілки поведінки — наприклад, «мобільний» рендер без реального пристрою.
Чи можна передавати тіло в GET-запиті?
Технічно — так: синтаксис HTTP не забороняє тіло в жодному методі. Семантично — ні: специфікація каже, що тіло в GET не має загальновизначеного значення, не може змінити зміст чи ціль запиту, і клієнту рекомендовано його не генерувати. На практиці це означає непередбачувану поведінку: одні сервери й проксі проігнорують тіло, інші відкинуть запит (зокрема через ризик request smuggling), треті оброблять. Відомий виняток — Elasticsearch, який дозволяє GET із тілом для пошукових запитів і саме через цю невизначеність паралельно підтримує той самий запит через POST. Тож правило для практики: потрібно передати складну структуру для «читання» — використовуйте POST, а не тіло в GET.
Звідки в Network «зайвий» OPTIONS перед POST і що таке CORS preflight?
Це не зайвий запит, а автоматична розвідка браузера. Коли браузер збирається зробити «непростий» крос-доменний запит — наприклад, PATCH із Content-Type: application/json на інший домен — він спершу сам надсилає OPTIONS із заголовками Access-Control-Request-Method і Access-Control-Request-Headers, і лише якщо сервер відповість дозвільними Access-Control-Allow-*, відправляє справжній запит. Важлива деталь: application/json не входить до «безпечного» (safelisted) переліку типів, тому запит із таким тілом завжди тягне preflight — на відміну від application/x-www-form-urlencoded, multipart/form-data чи text/plain. Поза CORS OPTIONS також відповідає на питання «які методи приймає ресурс» — сервер повертає їх у заголовку Allow. Для AQA це часте джерело плутанини: тест, який «падає на CORS», насправді падає на preflight, і розуміння цього економить години на хибних діагнозах.
Як влаштоване тіло multipart/form-data і навіщо там boundary?
Коли форма містить файл, звичайного application/x-www-form-urlencoded замало — бінарні дані так надійно не передати. У multipart/form-data тіло розбивається на частини (parts), кожна зі своїми міні-заголовками (Content-Disposition з name, для файлів — ще filename і власний Content-Type), а розділяє їх унікальний рядок-роздільник boundary, оголошений у Content-Type всього запиту. На дроті кожна частина починається двома дефісами плюс значенням boundary, а весь блок завершується тим самим роздільником із ще двома дефісами в кінці. Boundary мусить бути таким, що гарантовано не трапиться у вмісті даних — тому браузери генерують довгі випадкові рядки. Для автотестів висновок практичний: якщо збирати multipart вручну (кривий curl), легко зіпсувати boundary чи заголовки частин і отримати 400, тож у Playwright і HTTP-клієнтах користуйтеся вбудованими засобами передачі файлу — вони складуть коректний multipart самі.
Чим відрізняються HTTP/1.1, HTTP/2 і HTTP/3?
Семантика (методи, заголовки, коди статусів) у всіх трьох спільна — змінюється спосіб передачі байтів по мережі. HTTP/1.1 текстовий і послідовний: у межах одного зʼєднання запити йдуть по черзі, і якщо перший «застряг», решта чекає (head-of-line blocking) — браузери обходили це кількома паралельними зʼєднаннями. HTTP/2 перейшов на бінарні кадри й мультиплексування (багато запитів паралельно одним TCP-зʼєднанням) і додав стиснення заголовків HPACK; head-of-line blocking усунено на рівні HTTP, але він лишається на рівні TCP. HTTP/3 переносить усе на QUIC поверх UDP: втрата одного пакета не гальмує решту потоків, а рукостискання TLS обʼєднане з транспортним, тож зʼєднання встановлюється швидше; заголовки стискає QPACK. Нюанс про server push із HTTP/2: великі браузери його згорнули (Chrome і Edge вимкнули з версії 106, механізм задепрекейчено), тож не покладайтеся на нього в тестах. Для функціональних автотестів версія зазвичай прозора, але при розслідуванні повільних чи нестабільних відповідей колонка Protocol у DevTools (h2, h3, http/1.1) — корисна зачіпка.
Що таке keep-alive і чому він критичний для навантажувальних тестів?
Keep-alive — це перевикористання одного TCP-зʼєднання під кілька запитів поспіль замість відкривання нового щоразу; оскільки встановлення TCP- і TLS-зʼєднання коштує часу, тримати його відкритим — суттєвий виграш. У HTTP/1.0 зʼєднання за замовчуванням закривалося і keep-alive вмикали явно заголовком Connection: keep-alive; у HTTP/1.1 постійні зʼєднання (persistent connections) стали типовими — зʼєднання живе, доки одна зі сторін не надішле Connection: close. У HTTP/2 і HTTP/3 ідея «одне зʼєднання — багато запитів» вбудована через мультиплексування, тому заголовок Connection там не застосовується — у HTTP/2 він навіть заборонений як connection-specific. Практичний наслідок: HTTP-клієнт з увімкненим keep-alive і пулом зʼєднань дає радикально інші, реалістичніші цифри, ніж клієнт, що відкриває зʼєднання на кожен запит. Тож якщо перформанс-тест показує підозріло високу затримку — спершу перевірте, чи не створюєте ви зʼєднання заново щоразу, а вже потім звинувачуйте сервер.
Три кейси, які показують HTTP у роботі AQA: читання тричленної структури запиту й відповіді через curl -v із тріажем 4xx vs 5xx, «зайвий» OPTIONS-preflight, на якому насправді падає тест «через CORS», і API-фікстура на Authorization із типовою пасткою Content-Type. Скрізь — що дивитися і чому.
Кейс 1. Читаєш запит і відповідь по кістках через curl -v
Тест падає на кроці логіну, а що саме пішло не так — незрозуміло. curl -v показує тричленну структуру буквально: рядки запиту з префіксом >, рядки відповіді з <.
curl -v -X POST https://app.example.com/api/login \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"email":"qa@example.com","password":"secret"}'
> POST /api/login HTTP/1.1
> Host: app.example.com
> Content-Type: application/json
> Accept: application/json
> Content-Length: 46
>
< HTTP/1.1 401 Unauthorized
< Content-Type: application/json; charset=utf-8
< WWW-Authenticate: Bearer
<
{"error":"invalid credentials"}
Що дивитися і чому:
- Стартовий рядок відповіді — перша лінія діагностики. Код
401— це клас4xx: «клієнт зробив щось не так», тобто провина запиту (не той токен, хибні дані), а не сервера. Якби прилетіло5xx— це вже ймовірний баг застосунку на коректному запиті, і баг варто заводити на продукт, а не на тест. - Порожній рядок перед тілом. Той самий
>без нічого (і<без нічого) — це не косметика, а синтаксис: він каже парсеру «заголовки скінчилися, далі тіло». Немає порожнього рядка — тіло не почнеться. WWW-Authenticateу відповіді. Сервер підказує, яку схему автентифікації він чекає (тутBearer). Це прямий натяк, чого бракує запиту.Content-Length: 46— розмір тіла в байтах; приймач знає, скільки читати. Якщо формуєш тіло вручну й число не збігається з реальним тілом — джерело обрізаних чи «завислих» запитів.
Той самий розклад дає DevTools: вкладка Network ділить запит на Headers / Payload / Response — те саме «стартовий рядок + заголовки + тіло», лише в UI.
Кейс 2. «Тест падає на CORS» — насправді на preflight
У Network перед кожним POST/PATCH на API видно «зайвий» OPTIONS, а тест червоніє «через CORS». Це не баг застосунку — це CORS preflight: перед «непростим» крос-доменним запитом браузер сам шле OPTIONS-розвідку й лише після дозвільної відповіді відправляє справжній запит.
OPTIONS /api/users/42 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: content-type
Що дивитися і чому:
application/jsonзавжди тягне preflight. Він не входить до «безпечного» (safelisted) переліку типів — на відміну відapplication/x-www-form-urlencoded,multipart/form-dataчиtext/plain. ТомуPATCHіз тілом-JSON гарантовано породжує пару «OPTIONS→ реальний запит».- Дивись, який запит червоний. Якщо впав саме
OPTIONS(немаєAccess-Control-Allow-*або в них не той метод/заголовок) — до справжньогоPATCHбраузер навіть не дійшов. Діагноз — конфіг CORS на сервері, а не логіка застосунку. - API-тест не бачить preflight. Прямий запит через API-клієнт (без браузера) не робить preflight узагалі — крос-доменні правила накладає браузер. Тому баг, що відтворюється тільки в
page-тесті й зникає в API-тесті, — типова ознака, що причина в preflight, а не в бекенді.
У Playwright перехоплення допомагає ізолювати гіпотезу — приберемо CORS зі змінних і подивимось, чи справа саме в ньому:
test('форма зберігається, коли API доступний', async ({ page }) => {
await page.route('**/api/users/42', (route) => {
// preflight-розвідку відповідаємо дозвільно
if (route.request().method() === 'OPTIONS') {
return route.fulfill({
status: 204,
headers: {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'PATCH, OPTIONS',
'access-control-allow-headers': 'content-type',
},
});
}
return route.fulfill({ status: 200, contentType: 'application/json', body: '{"ok":true}' });
});
await page.goto('/users/42/edit');
await page.getByRole('button', { name: 'Зберегти' }).click();
await expect(page.getByText('Збережено')).toBeVisible();
});
Якщо з таким моком тест зеленіє — проблема була в CORS-відповіді реального сервера, а не в UI.
Кейс 3. API-фікстура на Authorization і пастка Content-Type
Логінитися через UI перед кожним тестом повільно й крихко. Швидше й стабільніше — один раз узяти токен по API й підкладати його в заголовок наступних запитів. Playwright має для цього request-контекст:
import { test, expect, request } from '@playwright/test';
test('оновлення профілю через API-фікстуру', async ({ playwright }) => {
const api = await request.newContext({ baseURL: 'https://api.example.com' });
// 1. один логін — дістаємо Bearer-токен
const login = await api.post('/api/login', {
data: { email: 'qa@example.com', password: 'secret' },
});
expect(login.status()).toBe(200);
const { token } = await login.json();
// 2. далі носимо токен у заголовку Authorization
const res = await api.patch('/api/users/42', {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
data: { role: 'admin' },
});
expect(res.status()).toBe(200);
});
Що дивитися і чому:
Content-Typeмусить відповідати тілу. Шлеш JSON, але забув заголовок (або поставивtext/plain) — сервер може не розпарсити тіло й повернути400. Класичний недогляд, коли тіло формують руками. У прикладіdata— це JSON, тож і заголовокapplication/json.PATCHшле лише дельту. Тіло{"role":"admin"}чіпає одне поле. Якби це бувPUT, довелося б передати ЦІЛЕ представлення (name,email,role) — інакше ризик затерти непередані поля. Тому для точкової зміни бериPATCH, для повної заміни —PUT.Bearer= «предʼявник». Хто володіє токеном — той має доступ; ніякого логіна/пароля в кожному запиті. На відміну відBasic, де в заголовку летитьbase64("логін:пароль")— це кодування, а не шифрування, тож без HTTPS пароль фактично відкритий.- Ретрай за таймауту — тільки для ідемпотентних.
PATCHу загальному випадку неідемпотентний (патч «+1 до лічильника» щоразу дає новий стан), тож сліпо ретраїти його на мережевому збої небезпечно — можна змінити дані двічі.GET/PUT/DELETEповторювати безпечно. Перед авто-ретраєм у фікстурі звір семантику методу.
Структура повідомлення й статуси
- Розумію, що HTTP — stateless: сервер за замовчуванням не памʼятає попередній запит, тому автотест сам дбає про автентифікацію, куки й стан.
- Знаю три частини будь-якого повідомлення: стартовий рядок, заголовки, тіло — і що порожній рядок перед тілом є частиною синтаксису, а не косметикою.
- Можу розкласти request-line (метод + ціль + версія) і status-line (версія + код + reason phrase) і памʼятаю, що клієнт орієнтується на числовий код, а фразу може ігнорувати.
- Розумію, що тричленна текстова структура — це HTTP/1.1: у HTTP/2 і HTTP/3 стартового рядка немає, натомість псевдозаголовки
:method,:path,:status, а reason phrase не існує. - Можу пояснити пʼять класів кодів за першою цифрою і головне для діагностики:
4xx— винен клієнт (мій запит),5xx— упав сервер на коректному запиті (ймовірний баг застосунку, не тесту).
Методи та їх семантика
- Можу пояснити safe-метод (
GET,HEAD,OPTIONS,TRACE— лише читають, без побічних ефектів) і що це про семантику, а не гарантію:GET, який мутує стан, — це баг. - Розумію ідемпотентність як однаковий стан сервера після одного й після N викликів; знаю, що
PUT/DELETEідемпотентні, аPOSTі зазвичайPATCH— ні. - Можу пояснити пастку
DELETE: другий виклик поверне404, але метод усе одно ідемпотентний — ідемпотентність про стан, не про код відповіді. - Розумію практичний наслідок: ідемпотентні запити безпечно ретраїти за таймауту, тому клієнти ретраять
GET/PUT/DELETE, але неPOST(звідси дублі при подвійному кліку «Оплатити»). - Можу розкрити GET vs POST далі за «читає/пише»: safe, ідемпотентність, дані в query проти тіла, кешування, осідання в історії/логах — і чому секрети йдуть тілом, а не в query.
- Розумію різницю
PUT(заміна ресурсу цілком, ризик затерти непередані поля) іPATCH(часткова дельта), і що формат тілаPATCHне єдиний — дивлюсь контракт API. - Можу пояснити, чому тіло в
GETтехнічно можливе, але семантично безглузде й непередбачуване на проксі — складне «читання» роблю черезPOST. - Розумію дві ролі
OPTIONS: заголовокAllowі, головне, CORS preflight — «зайвий»OPTIONSпередPOST/PATCH, і що тест «падає на CORS» насправді падає на preflight. - Знаю, навіщо
HEADв AQA: ті самі заголовки, що йGET, але без тіла — дешева перевірка існування/розміру/типу файлу в health-check і смоук-тестах.
Заголовки
- Розумію, що
Content-Typeописує медіа-тип наявного тіла (двосторонній: у запиті й відповіді), і що хибнийContent-Type(JSON ізtext/plain) — класична причина400. - Можу розвести
Accept(побажання клієнта щодо формату відповіді, content negotiation, вагиq) іContent-Type(що зараз лежить у тілі) — «що хочу отримати» проти «що вклав у конверт». - Розумію
Authorization:Bearer(токен-предʼявник, часто JWT) іBasic(base64("логін:пароль")— кодування, не шифрування); знаю, що це основа API-фікстур замість логіну через UI. - Розумію, чому
User-Agent— двобічний меч: дефолтний UA бібліотеки (curl,PostmanRuntime) може дати іншу відповідь, ніж браузер, або навпаки — перемкнути гілку поведінки.
Query, multipart і версії
- Можу провести межу query vs тіло: query — параметри читання (фільтр, сортування, пагінація), видимі в URL і логах, обмежені довжиною; тіло — дані, що змінюють стан, і складні структури; секрети завжди в тіло.
- Знаю, що спецсимволи в query мають бути percent-encoded (пробіл →
%20), і що пропущене кодування «губить» параметр або ламає запит. - Розумію
multipart/form-dataдля файлів: частини з міні-заголовками, розділені унікальним boundary; ручне складання легко ламає boundary →400, тому користуюсь вбудованими засобами клієнта. - Можу пояснити, що логіка HTTP/1.1 vs 2 vs 3 спільна, а різниться передача байтів: текст проти бінарних кадрів, TCP проти QUIC, послідовність проти мультиплексування, стиснення заголовків.
- Розумію keep-alive як перевикористання TCP-зʼєднання (у HTTP/1.1 типове, у HTTP/2/3 вбудоване через мультиплексування) і що клієнт без keep-alive дає нереалістично високу затримку в перформанс-тестах.
Що означає властивість HTTP «stateless»?
Питання
З яких трьох частин складається будь-яке HTTP-повідомлення і що відокремлює заголовки від тіла?