vyvchy
    Теми розділу

    03 · Веб і мережі для AQA

    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-повідомлення — і запит, і відповідь — складається з трьох частин у сталому порядку:

    1. Стартовий рядок (start-line) — перший рядок.
    2. Заголовки (headers) — набір пар «назва: значення», по одній на рядок.
    3. Тіло (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 — «сервер упав на коректному запиті», тобто ймовірний баг застосунку, а не тесту. Плутати ці класи — значить заводити баги на власні тести або, навпаки, списувати падіння продукту на «нестабільність».

    2xx

    3xx

    4xx

    5xx

    Отримано код статусу

    Перша цифра?

    Успіх

    Перенаправлення

    Помилка клієнта: перевір шлях, токен, тіло запиту

    Помилка сервера: ймовірний баг застосунку

    2xx

    3xx

    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 пише» (це спрощення), а в цілому наборі властивостей:

    КритерійGETPOST
    Семантикаотримати ресурснадіслати дані на обробку
    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

    Але найважливіша для фронтенд-тестування роль OPTIONSCORS 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. Розуміння цього економить години на хибних діагнозах.

    СерверБраузерСерверБраузерДозвіл отриманоOPTIONS (preflight) + Access-Control-Request-*Access-Control-Allow-*Справжній PATCH-запитВідповідьСерверБраузерСерверБраузерДозвіл отриманоOPTIONS (preflight) + Access-Control-Request-*Access-Control-Allow-*Справжній PATCH-запитВідповідь

    Метод 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-TypeAccept
    Що описуєщо є в тілі заразщо клієнт хоче отримати
    Напрямокопис наявного тілапобажання щодо майбутньої відповіді
    Де єу запиті й у відповідіпереважно у запиті
    Приклад«я надсилаю 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, після ?після заголовків
    З якими методамибудь-якими, типово GETPOST/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.1HTTP/2HTTP/3
    Форматтекстовийбінарні кадри (frames)бінарні кадри
    ТранспортTCPTCPQUIC (поверх UDP)
    Одночасні запити на зʼєднанніпо черзі (1 за раз)мультиплексуваннямультиплексування
    Стиснення заголовківнемаєHPACKQPACK
    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 та пулом зʼєднань дає радикально інші (реалістичніші й швидші) цифри, ніж клієнт, що відкриває нове зʼєднання на кожен запит. Якщо ваш перформанс-тест показує підозріло високу затримку — перевірте, чи не створюєте ви зʼєднання заново щоразу.