HTTP: методи, структура, заголовки
Зміст
Кожен клік урешті стає HTTP-повідомленням. Логін, збереження форми, підвантаження списку — це все запити, і саме тут видно те, чого не видно в інтерфейсі: чи фронтенд шле той метод, чи дані поїхали в адресі замість тіла, чи токен узагалі доїхав. Тест, що логіниться без UI, , яка готує стан одним запитом, баг-репорт із доказом замість «у мене не працює» — усе це починається з уміння прочитати повідомлення по кістках.
Ця глава — повний виклад теми на сайті, інші глави посилаються сюди по структуру повідомлення й семантику методів. Мета одна: дивлячись на запит, за пів хвилини сказати, що в ньому не так.
Одна структура на запит і на відповідь
HTTP-повідомлення (message) — це спосіб обміну даними між клієнтом і сервером; типів рівно два: запит (request), яким клієнт ініціює дію, і відповідь (response), якою сервер відповідає. Будова в них однакова: стартовий рядок (start-line), необовʼязковий набір заголовків (headers), порожній рядок і необовʼязкове тіло (body). Стартовий рядок разом із заголовками називають «головою» (head) повідомлення, усе після — тілом.
Порожній рядок — не косметика, а частина синтаксису: саме він каже парсеру, що метадані скінчилися й далі йдуть дані.
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"}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 52
{"token":"eyJhbGciOiJIUzI1NiJ9...","expiresIn":3600}
Стартовий рядок запиту зветься request-line і має три частини: метод, ціль запиту (request target — зазвичай шлях із рядком запиту) і версію протоколу. Стартовий рядок відповіді — це status-line, і в ньому теж три частини: версія, і (reason phrase). Фраза суто інформативна — вона допомагає людині, а не машині, і RFC 9112 рекомендує клієнту її ігнорувати (SHOULD) як ненадійний канал. Практичний наслідок: у тесті число, а не рядок.
Заголовки — це поля, що передаються перед вмістом. У HTTP/1.x кожен заголовок — нечутлива до регістру назва, двокрапка й значення, усе в одному рядку (Content-Type і content-type — те саме); чутливість самого значення залежить від конкретного заголовка.
Метод: намір, а не гарантія
Метод (HTTP method) — перший елемент стартового рядка запиту; він каже, що клієнт хоче зробити з ресурсом за вказаною адресою. Методи ще звуть «дієсловами» (verbs). Крім власної семантики кожен метод описують трьома наскрізними властивостями: безпечність, і кешованість.
(safe method) — той, чия визначена семантика фактично «лише читання»: клієнт не просить і не очікує зміни стану ресурсу на сервері. Безпечні — GET, HEAD, OPTIONS і TRACE. Це точніше за звичне «без побічних ефектів», і різниця змістова: специфікація прямо дозволяє реалізації мати побічні ефекти під безпечним методом (її власний приклад — запис у лог доступу), важливо лише те, що клієнт цієї поведінки не замовляв і не відповідає за неї. Мету поділу джерело називає теж прямо: щоб (spiders) і працювали без страху щось зламати.
Звідси головне для тестування: HTTP описує наміри, а не гарантує поведінку реалізації. Ніщо не заважає зробити GET /users/42/delete, який видаляє користувача, — і саме тому це баг контракту, а не особливість. Краулер або префетч браузера «клікне» таке посилання без жодного наміру шкодити, тож заводьте баг, а не адаптуйте тест.
| Метод | Що робить | Безпечний | Ідемпотентний | Кешований |
|---|---|---|---|---|
GET | отримати подання ресурсу | так | так | так |
HEAD | те саме без тіла у відповіді | так | так | так |
OPTIONS | дізнатися доступні опції | так | так | ні |
TRACE | віддзеркалити повідомлення | так | так | ні |
PUT | створити або замінити цілком | ні | так | ні |
DELETE | розірвати звʼязок із ресурсом | ні | так | ні |
POST | надіслати на обробку | ні | ні | умовно |
PATCH | змінити частково | ні | ні | умовно |
CONNECT | службовий | ні | ні | ні |
Кешовані за замовчуванням лише GET і HEAD; POST і PATCH — умовно, коли відповідь явно містить дані про свіжість і відповідний Content-Location. Самі правила свіжості й кеш-заголовки розбирає глава «Кешування».
Розбір методів
GET запитує передачу поточного подання цільового ресурсу — це основний механізм отримання даних. Тіла він нести не повинен: синтаксис його не забороняє, але семантично воно не має загальновизначеного значення й не може змінити зміст чи ціль запиту, тож клієнту рекомендовано його не генерувати. Поведінка серверів і при цьому непередбачувана, тому складну структуру для «читання» передають через POST.
POST просить цільовий ресурс обробити передане подання за власною семантикою ресурсу — саме тому він ані безпечний, ані ідемпотентний: що станеться від повтору, протокол не знає.
PUT створює або повністю замінює стан ресурсу переданим поданням; після успішного PUT наступний GET має повернути еквівалентне подання. 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 ви зобовʼязані передати цілісне подання — інакше ризикуєте затерти те, чого не передали. Формат тіла PATCH одним стандартом не задано: є application/json-patch+json (RFC 6902) і application/merge-patch+json (RFC 7396), але багато API приймають власний частковий JSON, тож дивіться контракт, а не припускайте.
DELETE просить сервер розірвати звʼязок між цільовим ресурсом і його поточною функціональністю. Це не гарантія фізичного видалення даних — рядок у базі цілком може лишитися з прапорцем.
HEAD ідентичний GET, але надсилати тіло сервер не має права: тут MUST NOT. Заголовки він SHOULD віддати ті самі, що й для GET, — проте MAY пропустити поля, значення яких визначається лише під час генерації вмісту, і прикладом джерело називає рівно Content-Length. Тому HEAD — дешева перевірка існування ресурсу для , але асерт «файл важить рівно 1 МБ» на ньому потенційно : заголовка може просто не бути, і це право сервера, а не баг.
OPTIONS запитує доступні опції взаємодії з ресурсом без виконання дії над ним. У браузері це насамперед CORS preflight: специфікація зобовʼязує браузер «промацати» непростий запит, спитавши в сервера підтримувані методи саме через OPTIONS. Звідси «зайвий» OPTIONS перед POST чи PATCH у мережевій панелі — це preflight, а не баг; механіку дозволів розбирає глава «CORS і політика одного походження». Друга тонкість: відповідь OPTIONS — вхідна точка, а не вердикт, бо реальний перелік перевіряють, надсилаючи запити різними методами.
Службові. TRACE задуманий для тестування й відлагодження: він наказує серверу віддзеркалити отримане повідомлення клієнту — і попри позірну нешкідливість у певних сценаріях ним крадуть облікові дані. CONNECT не є ані безпечним, ані ідемпотентним, ані кешованим. Увімкнений службовий метод у проді — тема конфігураційних дір із розділу про безпеку, не протокольна.
Ідемпотентність: про стан сервера, а не про відповідь
Ідемпотентність (idempotency) — властивість, за якої кілька однакових запитів дають на сервері той самий очікуваний ефект, що й один такий запит. Інакше кажучи: стан сервера після одного виклику й після N однакових викликів поспіль однаковий.
Ось тонкість, на якій плутаються найчастіше: ідемпотентність стосується стану сервера, а не тіла відповіді — коди й заголовки повторних викликів можуть відрізнятися.
Ідемпотентні: GET, HEAD, OPTIONS, TRACE, PUT, DELETE. Неідемпотентних три, а не два, як зазвичай кажуть: POST, PATCH і CONNECT — останній випадає з переліків лише тому, що в прикладному тестуванні не трапляється.
PUTідемпотентний, бо замінює ресурс цілком: той самийPUT /users/42десять разів залишає ресурс у тому самому стані, що й після першого.DELETEідемпотентний попри різні коди: перший виклик поверне200/204, другий —404, але стан сервера незмінний — ресурсу немає й немає.POSTнеідемпотентний: два однаковіPOST /ordersстворять два замовлення. Звідси дублі від подвійного кліку по кнопці оплати без захисту.PATCHнеідемпотентний за специфікацією — RFC 5789 каже це прямо. Патч «додай +1 до лічильника» щоразу дає новий результат; конкретний запит можна сформувати ідемпотентним, але від методу цього ніхто не вимагає.
Усі безпечні методи ідемпотентні, але не навпаки: PUT і DELETE ідемпотентні й при цьому змінюють стан. Ідемпотентність і кешованість — теж різні осі: PUT і DELETE ідемпотентні, але не кешовані.
Практична цінність пряма. Ідемпотентні запити можна безпечно повторювати після чи мережевого збою, а POST — ні: якщо тест на мережі, знання методу відповідає на головне питання, чи безпечний тут . І асерт на повторний DELETE має враховувати семантику: 404 на другий виклик — очікувана поведінка, а не порушення ідемпотентності. Повторний PUT/DELETE як сценарій тестування API і Idempotency-Key — тема розділу «API-тестування».
Наша практика (не канон). Далі — те, як поводяться клієнти, з якими ми працювали; окремої норми під це немає. HTTP-бібліотеки часто вмикають автоматичний ретрай саме для GET, PUT і DELETE й не вмикають для POST — тож перевіряйте налаштування клієнта, перш ніж списувати дублі на застосунок.
Заголовки: метадані повідомлення
Заголовки — це метадані повідомлення: поля після стартового рядка й перед тілом, що уточнюють його семантику. Вони описують відправника, формат вмісту, автентифікацію, правила кешу.
Content-Type описує медіа-тип (media type, MIME type) тіла, яке передається зараз: application/json, application/x-www-form-urlencoded, multipart/form-data, text/html; charset=utf-8. Заголовок двосторонній — у запиті описує тіло клієнта, у відповіді тіло сервера. Помилковий Content-Type — класична причина , і нормативний код тут 415, а не 400: сервер відмовляється обслуговувати запит, бо вміст у форматі, який метод на цьому ресурсі не підтримує. Сервер, що віддає на це 400, від специфікації відступає, і в баг-репорті це варто називати.
Content-Length вказує розмір тіла в байтах — щоб знав, скільки читати.
Accept — навпаки, побажання клієнта: у якому форматі він хоче відповідь. Механізм зветься узгодженням вмісту (content negotiation), а ваги q нормовані в діапазон від 0 до 1; за відсутності q перевага дорівнює 1. Край діапазону варто знати точно: q=0 — це не «найменш бажаний», а «неприйнятно», тобто пряма відмова від формату (найменш бажаний із прийнятних — 0.001). Там же обмеження запису: більше трьох цифр після коми відправник генерувати не має права.
Content-Type | Accept | |
|---|---|---|
| Що описує | що є в тілі зараз | що клієнт хоче отримати |
| Напрямок | опис наявного тіла | побажання щодо майбутньої відповіді |
| Де є | у запиті й у відповіді | переважно в запиті |
| Мнемоніка | «ось що я вклав у конверт» | «ось у якому форматі хочу відповідь» |
У межах одного запиту вони спокійно відрізняються: клієнт шле форму (Content-Type: application/x-www-form-urlencoded), а відповідь хоче в JSON (Accept: application/json). Кеш-заголовки живуть у главі «Кешування», Set-Cookie — у главі «Кукі, сесії та сховище браузера».
Заголовки, якими тест керує запитом
Authorization несе облікові дані. Схема Bearer несе Bearer-токен (часто JWT), і специфікація задає форму дослівно: слово Bearer, пробіл, токен. Схема Basic — це base64("логін:пароль"), причому роздільником вважається перша двокрапка, тож двокрапка в логіні робить пару невалідною. тут кодування, а не шифрування: сама специфікація каже, що схема не вважається безпечною без зовнішнього захищеного каналу на кшталт TLS, бо логін і пароль ідуть мережею відкритим текстом (модальність — SHOULD NOT, не заборона). Чому саме TLS це лікує — глава «HTTPS, TLS і безпека».
Якщо сервер вимагає автентифікації, він відповідає 401 разом із WWW-Authenticate, який підказує схему. Проксі робить те саме іншою парою — 407 і Proxy-Authenticate; плутати їх не можна. Будову JWT усередині Bearer розбирає глава «Автентифікація та авторизація».
User-Agent ідентифікує клієнтське ПЗ, і сервери іноді змінюють поведінку залежно від нього — мобільна версія проти десктопної, блокування ботів. Для тестувальника це палиця на два кінці: автотест із дефолтним User-Agent бібліотеки (curl/…, PostmanRuntime/…) може отримати іншу відповідь, ніж реальний браузер; підміна того самого заголовка навпаки дає «мобільну» гілку без пристрою.
Але не все підміняється. Fetch перелічує forbidden request-header — заголовки, яких код задати не може: серед них Cookie, Host, Origin, Referer, Connection, Content-Length, Date, TE, Transfer-Encoding, а також усе, що починається з proxy- чи sec-. User-Agent у цьому переліку немає — і саме тому підміна працює: браузер підставляє власне значення лише тоді, коли в списку заголовків його ще немає.
Найчастіший практичний ужиток — фікстура автентифікації: замість логіну через UI тест один раз бере токен і додає його в заголовок наступних запитів.
// playwright.config.ts — усі запити тесту йдуть уже з токеном
export default defineConfig({
use: {
extraHTTPHeaders: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
Accept: 'application/json',
},
},
});
Наша практика (не канон). Канонічного джерела під це немає: для тесту заголовки — найдешевший важіль керування запитом. Підмінивши один рядок, тест логіниться без інтерфейсу, просить інший формат відповіді або вдає інший клієнт — і це майже завжди дешевше, ніж відтворювати ту саму умову клацанням.
Тіло: необовʼязкова частина після порожнього рядка
Тіло (body) несе власне дані. У запиті воно передає інформацію на сервер, у відповіді — запитані дані або пояснення, чому запит не виконано. Що саме в ньому лежить і скільки його — кажуть Content-Type і Content-Length; без них приймач не знає ні формату, ні межі даних.
Тіло є не в кожному повідомленні. Із запитів його мають POST, PUT і PATCH. З відповідей тіла може не бути в тих, що відповідають на запит без потреби у вмісті: MDN наводить прикладами 201 Created і 204 No Content. Для 204 це означення — тіла немає ніколи, значущі лише заголовки. Окремий випадок — відповідь на HEAD, де заборона нормативна. Практичний наслідок: спроба розпарсити JSON із 204 впаде, і це не баг сервера. Семантику самих кодів розбирає наступна глава — «HTTP статус-коди».
Межа «query проти тіла» і multipart
І рядок запиту (query), і тіло передають дані серверу — межа проходить не по смаку, а по трьох речах. Query — плоскі пари «ключ-значення» в URL, видимі всім, хто бачить адресу, і обмежені довжиною. Тіло — довільна структура (JSON, форма, файли) поза URL, з лімітом на порядки більшим.
Цифри тут не з повітря. Специфікація зверху довжину URL не обмежує, зате задає нижню межу підтримки: усім відправникам і отримувачам RECOMMENDED тримати щонайменше 8000 октетів. Дефолти серверів того самого порядку — в Apache LimitRequestLine 8190 байтів (а request-line це метод, URI та версія, тож ліміт покриває і query), у nginx рядок запиту не може перевищити один буфер із large_client_header_buffers 4 8k, інакше клієнт отримує 414. Тіло має власний ліміт: LimitRequestBody за замовчуванням 1073741824 байти. Тобто «великі дані — тілом» це не стильова порада, а різниця лімітів. Будову URL і кодування символів розбирає глава «URL і кодування».
Коли у формі є файл, звичайного application/x-www-form-urlencoded замало — тут працює multipart/form-data. Тіло розбивається на частини (parts), кожна зі своїми міні-заголовками, а розділяє їх унікальний рядок-розділювач (boundary), оголошений у Content-Type. Частина з файлом додатково має filename і власний Content-Type.
POST /api/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=abc123
--abc123
Content-Disposition: form-data; name="title"
Some report name
--abc123
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf
%PDF-1.4 ... (бінарний вміст файлу) ...
--abc123--
Роздільник — це два дефіси плюс значення boundary із заголовка, а завершальний додає ще два дефіси в кінці. Вимога унікальності нормативна: роздільник MUST NOT траплятися всередині жодної з частин — ні окремим рядком, ні як префікс рядка. Сам параметр — єдиний обовʼязковий для multipart, довжиною 1–70 символів і не може закінчуватися пробілом. Складене вручну тіло легко зіпсувати саме на boundary чи заголовках частин, тому у Playwright і HTTP-клієнтах передавайте файл обʼєктом і хай бібліотека збере його сама.
Наша практика (не канон). Джерела під це немає, але правило стійке: секрети йдуть тілом, бо query осідає в адресному рядку, історії браузера, логах сервера й реферерах. Токен у query-параметрі — готова знахідка для security-репорту.
Безстановість: увесь контекст їде в кожному повідомленні
HTTP визначений як безстановий (stateless) протокол: семантику кожного запиту можна зрозуміти ізольовано, і сервер не має припускати звʼязок між двома запитами на одному зʼєднанні. Те саме обмеження є в REST: кожен запит містить усю потрібну для його розуміння інформацію й не спирається на збережений на сервері контекст — стан сесії лишається на клієнті. Мотив прагматичний: якщо сервер не мусить памʼятати клієнтів, будь-який із десятка однакових серверів обробить будь-який запит.
Уточнення, без якого виходить плутанина: безстановий тут протокол, а не застосунок. Стан у застосунку є майже завжди — просто він живе в базі й у тому, що клієнт приносить із кожним запитом. А приносить він його рівно двома частинами повідомлення, які ми щойно розібрали: заголовками й тілом. Найпоширеніший механізм памʼяті — кукі: сервер ставить їх заголовком Set-Cookie, а браузер повертає на наступних запитах. Альтернатива — токен у Authorization, який клієнт зберігає й додає сам.
Для тестів це і полегшення, і пастка. Полегшення — бо ізольований запит легко повторити: підготувати стан одним замість проходу майстра з десяти екранів. Пастка — бо «залогінитися один раз» на рівні протоколу не існує: кожен запит має нести автентифікацію сам. Модель клієнт-сервер загалом розбирає глава «Клієнт-сервер і як працює веб».
Версія протоколу змінює подання, а не семантику
До HTTP/2 повідомлення текстові й читабельні очима; у HTTP/2 їх загортають у бінарні кадри — читати важче, а логічна модель лишається тією самою. У HTTP/2 стартового рядка як окремого рядка немає взагалі: керівні дані передають псевдозаголовками з двокрапкою на початку — :method, :scheme, :authority, :path у запиті й :status у відповіді. HTTP/3 уживає такі самі за призначенням поля й прямо каже про себе: способу передати версію чи пояснювальну фразу зі статус-рядка HTTP/1.1 він не визначає. Читати це як «а HTTP/2 щось таке має» не можна — версію RFC 9113 формулює теж, просто неявно: окремого індикатора в повідомленні немає, і запити з відповідями «implicitly have a protocol version of "2.0"» (§8.3.1 і §8.3.2). Різниця лише в другій половині: пояснювальної фрази RFC 9113 не згадує жодного разу, тож для HTTP/2 її відсутність — вивід із семантики HTTP, а не цитата зі специфікації.
Головне: HTTP/2 — не нова семантика, а оптимізоване подання тієї самої. Методи, статус-коди й заголовки ті самі; змінюється спосіб, у який вони їдуть зʼєднанням, і саме зʼєднання намагаються перевикористати (keep-alive, а в HTTP/2 ще й мультиплексування). Тому переїзд на HTTP/2 не зачіпає тести на методи, коди й заголовки — змінюється транспортне подання, а не контракт. Бінарний фреймінг, стиснення заголовків і решту механіки розбирає глава «HTTP/2 і HTTP/3: еволюція протоколу».
Типові помилки
Виглядає як «безпечний метод нічого не зіпсує» — а насправді «безпечний» описує семантику, яку замовляє клієнт, і не забороняє реалізації мати побічні ефекти. GET, що видаляє, технічно можливий; саме тому це баг контракту.
Виглядає як «DELETE не ідемпотентний, бо другий виклик дає 404» — а насправді ідемпотентність про стан сервера, а не про код відповіді. Стан після другого виклику той самий: ресурсу немає.
Виглядає як «мій PATCH ідемпотентний, я ж шлю той самий JSON» — а насправді ідемпотентність тут властивість методу, і специфікація її від PATCH не вимагає. Конкретний запит може виявитися ідемпотентним, але будувати на цьому ретрай не можна.
Виглядає як «сервер загубив розмір файлу» — а насправді Content-Length у відповіді на HEAD сервер MAY пропустити. Асерт на розмір без перевірки наявності заголовка — джерело флаку, а не знахідка.
Виглядає як «сервер не приймає валідний JSON — значить, 400» — а насправді на непідтримуваний формат тіла нормативний код 415, і 400 тут відступ від специфікації, який варто назвати в баг-репорті.
Виглядає як «зайвий OPTIONS у панелі — баг фронтенду» — а насправді це CORS preflight, обовʼязковий для непростих крос-оріджин запитів.
Виглядає як «тест бачить те саме, що браузер» — а насправді з іншим User-Agent сервер має право віддати іншу гілку поведінки.
Підсумок
- Запит і відповідь мають одну будову: стартовий рядок, заголовки, порожній рядок, тіло. Порожній рядок — частина синтаксису, а не форматування.
- Метод описує намір клієнта, а не гарантує поведінку реалізації. Тому «
GET, який видаляє» — баг контракту, а не особливість. - Ідемпотентність — про стан сервера після N однакових викликів, а не про однаковість відповідей.
DELETEіз404на другий виклик ідемпотентний. - Заголовки — метадані, що уточнюють семантику повідомлення: формат вмісту, автентифікація, опис клієнта. Частину з них код задати не може взагалі.
- Тіло необовʼязкове, і його межу задають
Content-TypeтаContent-Length; частина відповідей іде без тіла свідомо. Секрети — тілом, ніколи в рядку запиту.
Можливі питання
- З яких частин складається HTTP-повідомлення? Перевіряють не завчений список, а розуміння, що будова однакова для запиту й відповіді й що порожній рядок несе синтаксичну роль.
- Що таке безпечний метод і чому
GET, який змінює дані, — це баг? Тут чути, чи кандидат каже «без побічних ефектів» (спрощення), чи «семантика лише для читання з погляду клієнта». Плюс балів — за наслідок: префетч і краулери викличуть такий без наміру шкодити. - Що таке ідемпотентність і чому
DELETEідемпотентний, якщо другий раз повертає404? Класична перевірка на плутанину «стан сервера проти тіла відповіді». Гарна відповідь дає перелік і пояснює, чомуPOSTіз нього випадає. - Чим
PUTвідрізняється відPATCH? Цікавить не словник, а :PUTзамінює цілком, тож непередані поля втрачаються. - Коли дані передавати в рядку запиту, а коли тілом? Дивляться на три критерії: видимість, обмеження довжини, структура даних. Згадка про порядок 8 кБ на рядок запиту показує, що людина бачила реальні
414. - Чим
Content-Typeвідрізняється відAccept? Питання на уважність до напрямку: що вже лежить у тілі проти того, що клієнт хоче отримати. Сильна відповідь додає код відмови415. - Навіщо потрібен
HEAD? Перевіряють, чи розуміє кандидат, що тіла у відповіді бути не може, а заголовки гарантовані не всі. Найкращий фінал — пастка зContent-Length.
Джерела
Одна структура на запит і на відповідь
- MDN — HTTP messages — два типи повідомлень, спільна будова, голова проти тіла, склад request-line і status-line, формат заголовка в HTTP/1.x.
- RFC 9110 — HTTP Semantics — заголовки як поля, що передаються перед вмістом повідомлення.
- RFC 9112 — HTTP/1.1 (message syntax) — клієнту
SHOULDігнорувати пояснювальну фразу: це ненадійний канал.
- MDN — HTTP request methods — метод як перший елемент стартового рядка, три наскрізні властивості, матриця безпечності/ідемпотентності/кешованості, умовна кешованість
POSTіPATCH. - RFC 9110 — HTTP Semantics — означення безпечного методу через семантику «лише читання», дозвіл на побічні ефекти, мета поділу (spiders, префетч), перелік безпечних методів.
- RFC 9110 — HTTP Semantics — семантика
GET,POST,PUT,DELETE,OPTIONS; заборона тіла у відповіді наHEADі дозвіл пропуститиContent-Length; тіло вGETбез загальновизначеного значення. - MDN — HTTP request methods — довідник методів і формати тіла
PATCH(RFC 6902, RFC 7396) поруч із власним частковим JSON. - RFC 5789 — PATCH Method for HTTP — різниця тіл: змінена версія ресурсу в
PUTпроти набору інструкцій уPATCH. - MDN — Cross-Origin Resource Sharing (CORS) — preflight як обовʼязкове опитування сервера методом
OPTIONS. - OWASP WSTG — 4.2.6 Test HTTP Methods (WSTG-CONF-06) — перелік з
OPTIONSперевіряють фактичними запитами;TRACEяк віддзеркалення повідомлення й ризик крадіжки облікових даних.
Ідемпотентність: про стан сервера, а не про відповідь
- RFC 9110 — HTTP Semantics — означення ідемпотентності через ефект на сервері, перелік (
PUT,DELETEі безпечні методи), безпечність повторів після збою. - MDN — HTTP request methods — матриця ідемпотентності, приклади
POSTіPATCH, розведення ідемпотентності й кешованості. - MDN — Idempotent (Glossary) — поіменний перелік неідемпотентних методів разом із
CONNECT. - RFC 5789 — PATCH Method for HTTP —
PATCHне є ані безпечним, ані ідемпотентним; конкретний запит можна зробити ідемпотентним.
Заголовки: метадані повідомлення
- RFC 9110 — HTTP Semantics —
Content-Typeяк медіа-тип тіла,Content-Lengthяк розмір у байтах,Acceptі вагиq(зокремаq=0як відмова й три цифри після коми),415для непідтримуваного формату. - MDN — HTTP messages — заголовки як метадані між стартовим рядком і тілом.
Заголовки, якими тест керує запитом
- RFC 7617 — The 'Basic' HTTP Authentication Scheme — склад
Basic, роль першої двокрапки, base64 як кодування йSHOULD NOTбез TLS, пари401/WWW-Authenticateі407/Proxy-Authenticate. - RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage — форма запиту зі схемою
Bearer. - RFC 9110 — HTTP Semantics —
401ізWWW-Authenticateяк виклик до входу;User-Agentяк опис клієнта, за яким сервери підлаштовують відповіді. - WHATWG Fetch Standard — перелік forbidden request-header і правило, за яким браузер додає
User-Agentлише за відсутності власного значення. - Playwright — API testing — токен, отриманий один раз і підкладений у заголовки наступних запитів, замість логіну через UI.
Тіло: необовʼязкова частина після порожнього рядка
- MDN — HTTP messages — тіло як носій даних у запиті й відповіді, тіло лише в
PATCH,POSTіPUT, відповіді без тіла (201,204). - RFC 9110 — HTTP Semantics —
Content-TypeіContent-Lengthяк формат і межа даних; заборона тіла у відповіді наHEAD. - MDN — HTTP response status codes —
204 No Content: тіла немає, заголовки значущі.
Межа «query проти тіла» і multipart
- RFC 9110 — HTTP Semantics — рекомендований мінімум підтримки 8000 октетів у протокольних елементах.
- Apache HTTP Server 2.4 — Core Features (mod_core) —
LimitRequestLine8190 байтів іLimitRequestBody1073741824 байти за замовчуванням. - nginx — ngx_http_core_module —
large_client_header_buffersі414на завеликий рядок запиту. - RFC 7578 — Returning Values from Forms: multipart/form-data — поділ тіла на частини з власними заголовками,
filenameіContent-Typeчастини з файлом. - RFC 2046 — MIME Part Two: Media Types — роздільник із двох дефісів, завершальні два дефіси, заборона траплятися всередині частин, довжина 1–70 символів.
- Playwright — class APIRequestContext — передача файлу обʼєктом замість ручного складання
multipart.
Безстановість: увесь контекст їде в кожному повідомленні
- RFC 9110 — HTTP Semantics — HTTP як безстановий протокол: кожен запит зрозумілий ізольовано, звʼязок між запитами на одному зʼєднанні не припускається; масштабованість як мотив.
- Roy T. Fielding — Architectural Styles and the Design of Network-based Software Architectures, ch.5 (REST) — обмеження stateless: уся потрібна інформація в запиті, стан сесії цілком на клієнті.
- MDN — An overview of HTTP — безстановий протокол проти застосунку зі станом.
- RFC 6265 — HTTP State Management Mechanism —
Set-Cookieі повернення кукі агентом користувача на наступних запитах. - Playwright — API testing — підготовка стану й сесії запитом до API замість проходу через UI.
Версія протоколу змінює подання, а не семантику
- MDN — HTTP messages — текстові повідомлення до HTTP/2 і бінарне обгортання в HTTP/2; заміна стартового рядка псевдозаголовками.
- RFC 9113 — HTTP/2 — псевдозаголовки
:method,:scheme,:authority,:path,:status; неявна версія протоколу «2.0» для запитів (§8.3.1) і відповідей (§8.3.2); та сама семантика в іншому поданні. - RFC 9114 — HTTP/3 — ті самі за призначенням псевдозаголовки; специфікація не визначає способу передати версію чи пояснювальну фразу зі статус-рядка HTTP/1.1.
- MDN — Connection management in HTTP/1.x — зʼєднання замість встановлення нового на кожен запит.
З яких частин складається HTTP-повідомлення?
Схема одна й та сама для запиту і для відповіді: стартовий рядок, набір заголовків, порожній рядок, далі необовʼязкове тіло. Відрізняється лише перший рядок — у запиті це метод, ціль і версія протоколу, у відповіді версія, тризначний код і при ньому. Перші дві частини разом звуть головою повідомлення, усе після порожнього рядка — тілом. Сам порожній рядок не косметика: він і є межа, на якій парсер перестає читати метадані й починає читати дані, тож при ручному складанні запиту його втрата ламає розбір ще до того, як сервер дійде до тіла. Практичне продовження для тестів — пояснювальну фразу зі стартового рядка відповіді не : RFC 9112 радить клієнту на неї не спиратися, тому перевіряють число.
Що таке метод і які властивості його описують?
Метод стоїть першим у стартовому рядку запиту й повідомляє, яку саме дію клієнт замовляє над ресурсом за цією адресою; у розмові методи часто називають дієсловами. Крім власного змісту кожен метод характеризують трьома наскрізними ознаками: безпечність, і кешованість. Безпечні — GET, HEAD, OPTIONS, TRACE; ідемпотентні — ті самі плюс PUT і DELETE; кешованими за замовчуванням є лише GET і HEAD, а POST із PATCH стають такими за окремих умов. Ці три осі не збігаються, і плутати їх — типова помилка: PUT ідемпотентний, але ні безпечний, ні кешований. Для тестувальника ця матриця не теорія, а швидка відповідь на два робочі питання: чи можна цей запит повторити й чи має він узагалі щось змінювати.
Що означає «безпечний метод»?
Безпечність описує семантику «лише читання» з погляду клієнта: надсилаючи такий запит, він не замовляє зміну стану на сервері й не розраховує на неї. Формулювання «без побічних ефектів» — зручне спрощення, але неточне, бо реалізації прямо дозволено робити щось стороннє й під таким методом — за приклад специфікація бере запис у журнал звернень. Різниця в тому, хто відповідальний: побічний ефект є, але клієнт його не просив і не мусить за нього відповідати. Задум поділу теж названо прямо — щоб й у браузері могли ходити такими адресами, нічого не ламаючи. Звідси й головна практична межа: безпечність — це обіцянка контракту, а не властивість конкретного коду на бекенді.
Чому GET, який видаляє запис, — це баг, а не особливість реалізації?
Тому що протокол фіксує наміри, а не гарантує поведінку реалізації: технічно ніщо не заважає повісити видалення на GET /users/42/delete, і сервер це виконає. Проблема в тому, що весь світ довкола покладається на обіцянку безпечності. Пошуковий робот обійде посилання, браузер попередньо завантажить його, антивірусний чи превʼю в месенджері смикнуть адресу — і запис зникне без жодного наміру шкодити. Тому правильна реакція QA — завести баг контракту, а не підлаштовувати тест під наявну поведінку. У баг-репорті варто писати не «незвична реалізація», а конкретний : дію зі зміною стану може викликати будь-хто, хто просто побачив адресу.
Що таке ідемпотентність і які методи ідемпотентні?
Ідемпотентність означає, що кілька однакових запитів залишають сервер у тому самому стані, що й один такий запит: після десяти викликів поспіль результат на сервері має бути той самий, що після першого. Ідемпотентні GET, HEAD, OPTIONS, TRACE, PUT і DELETE; не ідемпотентні POST, PATCH і CONNECT — останній зазвичай випадає з переліків просто тому, що в прикладному тестуванні не трапляється. Усі ідемпотентні, але зворотне неправильно: PUT і DELETE цілком собі змінюють стан. Практична цінність властивості пряма: після мережевого ідемпотентний запит можна повторити без наслідків, а POST — ні, бо повтор створить другу сутність. Саме тому знання методу відповідає на головне питання при на мережі — чи безпечний тут .
DELETE вдруге повертає 404. Він же тоді не ідемпотентний?
Ідемпотентний, і це класична підміна понять. Властивість описує ефект на сервері, а не однаковість відповідей: коди й заголовки повторних викликів можуть відрізнятися скільки завгодно. Перший DELETE прибирає ресурс і віддає 200 чи 204, другий уже не має що прибирати й віддає 404 — але стан у момент після першого й після другого виклику однаковий: ресурсу немає. Для автотесту з цього випливає конкретна вимога до асерту: сценарій повторного видалення має очікувати саме таку пару кодів, а не трактувати 404 як порушення. І дзеркально — асерт «повтор дає той самий код, що й перший виклик» тут зайвий і зробить тест ламким без жодної користі.
Чим PUT відрізняється від PATCH?
PUT створює ресурс або цілком замінює його переданим поданням, PATCH застосовує часткову зміну. Різницю видно в тілі: у PUT ви шлете змінену версію ресурсу, у PATCH — набір інструкцій, що з ним зробити. Звідси головний ризик, заради якого це й питають: неповний PUT затирає все, чого ви не передали, тож оновлення однієї ролі повним заміщенням легко знесе решту полів обʼєкта. Формат тіла PATCH одним стандартом не закріплений — існують application/json-patch+json і application/merge-patch+json, але чимало API приймають власний частковий JSON, тому припускати формат не можна, треба дивитися контракт. Хороший негативний тест на PUT перевіряє рівно це: надсилаємо подання без частини полів і дивимось, що станеться з ними в базі.
Чи можна вважати PATCH ідемпотентним, якщо тіло щоразу однакове?
Ні, і це тонкий момент, на якому валяться навіть досвідчені. Ідемпотентність тут — властивість методу, і специфікація PATCH її від нього не вимагає; те, що ваш конкретний запит виглядає нешкідливим при повторі, нічого не змінює. Достатньо контрприкладу: інструкція «збільш лічильник на одиницю» щоразу дає новий результат, хоча тіло не змінювалось. Окремий запит справді можна сформувати так, щоб повтор нічого не зіпсував, але будувати на цьому автоматичний ретрай не можна — бекенд має право змінити семантику патча, не порушивши жодної норми. Практичний висновок для клієнта в тестах: політику повторів вмикайте за методом, а не за власним враженням від тіла.
Навіщо існує HEAD і де на ньому спотикаються автотести?
HEAD — це GET без вмісту: запит іде за тією ж адресою, але надсилати тіло у відповідь серверу заборонено нормативно. Цінність у дешевизні — можна перевірити, що ресурс існує, що посилання не побите, що файл доступний, не витягуючи мегабайти. Заголовки сервер має віддати ті самі, що й на GET, але з важливим застереженням: поля, значення яких зʼявляється лише при генерації вмісту, він має право пропустити, і специфікація прямо називає прикладом Content-Length. Ось де пастка: асерт «файл важить рівно стільки-то», побудований на HEAD, може падати через відсутність заголовка, і це буде реалізоване право сервера, а не знахідка. Правильний тест спершу перевіряє наявність поля й лише потім його значення — або взагалі бере розмір із GET.
Звідки в панелі мережі береться OPTIONS, якого код не надсилав?
Це майже завжди CORS preflight. Перед непростим запитом браузер зобовʼязаний окремо спитати сервер, чи дозволено такий метод і такі заголовки, і робить це саме методом OPTIONS. Тобто «зайвий» рядок перед вашим POST або PATCH — не баг фронтенду й не подвійне надсилання, а обовʼязковий крок протоколу; шукати його в коді застосунку марно. Сам по собі метод має ширше призначення — дізнатися доступні опції взаємодії з ресурсом, нічого над ним не виконуючи. Друга тонкість, за яку дають бали на співбесіді: відповідь OPTIONS — це заявка, а не вердикт, і при перевірці безпеки реальний перелік дозволених методів зʼясовують фактичними запитами, а не довірою до списку.
Чим Content-Type відрізняється від Accept?
Вони дивляться в різні боки в часі. Content-Type описує те, що вже лежить у тілі цього повідомлення, і працює в обидва боки — у запиті характеризує дані клієнта, у відповіді дані сервера. Accept — побажання клієнта щодо формату майбутньої відповіді, частина механізму узгодження вмісту, і живе він переважно в запиті. Побутова мнемоніка: перший каже «ось що я поклав у конверт», другий — «а відповідь надішліть у такому вигляді». У межах одного запиту вони спокійно відрізняються: форма їде як application/x-www-form-urlencoded, а відповідь очікується JSON-ом. Сильна відповідь додає код : якщо серверу не підходить формат тіла, нормативна реакція — 415.
Сервер відповів 400 на тіло в непідтримуваному форматі. Це коректно?
Формально — ні, і це готова знахідка. Коли тіло приїхало у форматі, з яким цей метод на цьому ресурсі не працює, специфікація має для відмови окремий код 415, тоді як 400 означає загальну проблему з запитом. Різниця не академічна: 415 одразу веде клієнта до заголовка Content-Type, а 400 змушує перебирати весь запит — тіло, параметри, довжину, кодування. На практиці бекенди схиблюють тут часто, тож заводити критичний баг не варто, але назвати відступ у репорті слід — це реальна вартість підтримки для команди інтеграції. Для власних негативних тестів висновок такий: очікуваним результатом на кривий Content-Type пишіть 415 і звіряйтеся з контрактом API, а не з поточною поведінкою.
Що означає q=0 у заголовку Accept?
Це відмова, а не «найменш бажаний варіант». Ваги в узгодженні вмісту нормовані в діапазон від нуля до одиниці, за відсутності ваги перевага дорівнює одиниці, а нуль означає, що формат клієнтові неприйнятний узагалі. Найменш бажане з прийнятного позначають 0.001 — саме тому край діапазону варто памʼятати точно. Поруч живе обмеження на форму запису: вагу з чотирма й більше знаками після коми відправникові складати не дозволено. Практичний наслідок для тестів простий — рядок на кшталт Accept: application/json, text/html;q=0 не є способом «злегка знизити пріоритет» HTML, це пряма заборона, і сервер має повне право відповісти відмовою в узгодженні.
Чому схему Basic не можна вважати захистом сама по собі?
Тому що в ній немає нічого, крім кодування. Клієнт склеює логін і пароль через двокрапку, кодує пару в і кладе в Authorization — а base64 розгортається назад будь-ким за секунду, це не шифрування. Специфікація каже це прямо: без захищеного транспорту знизу — TLS — схему вживати не варто, інакше логін і пароль мандрують мережею фактично відкритим текстом. Є ще дрібна деталь, яка регулярно ламає тести: роздільником вважається перша двокрапка, тож двокрапка всередині логіна робить пару невалідною, а всередині пароля — ні. Коли доступ закритий, у відповідь прилітає 401, а схему для входу сервер називає в заголовку WWW-Authenticate; проксі робить те саме своєю парою — 407 і Proxy-Authenticate, і плутати їх не можна, бо це різні місця для пошуку причини.
Тест підміняє заголовок у перехопленні маршруту, а запит іде зі старим значенням. Чому?
Найімовірніше, ви натрапили на forbidden request-header — перелік полів, якими розпоряджається сам браузер, бо на них тримаються межі безпеки. Туди потрапили Host, Cookie, Referer, Origin, Date, Connection, Content-Length, TE і Transfer-Encoding, а на додачу дві цілі групи — усе з префіксом proxy- і все з префіксом sec-. Код сторінки їх не задає, і обробник маршруту в тесті теж: Playwright прямо каже, що підміну забороненого заголовка буде проігноровано, а запит поїде з оригінальним значенням. Найгірше тут не сама заборона, а те, що вона тиха: помилки немає, тест зеленіє на неперевіреному сценарії. User-Agent у списку немає — саме тому підміна клієнта працює: браузер вставляє власне значення лише тоді, коли в наборі заголовків його ще немає. Практичний висновок: сценарії з чужою роллю через підміну Cookie треба будувати на рівні контексту чи сесії, а не на перехопленні запиту, і обовʼязково мати асерт, який доводить, що підміна справді доїхала.
Що має їхати в рядку запиту, а що тілом?
Межа проходить по трьох речах: структура, видимість і довжина. Query тягне пласкі пари «ключ-значення», лежить прямо в адресі й обмежений, тіло везе довільну структуру — JSON, форму, файли — і має ліміт на порядки більший. Цифри тут реальні: специфікація зверху довжину URL не обмежує, але радить усім підтримувати щонайменше вісім тисяч октетів, і дефолти серверів того самого порядку — Apache ріже стартовий рядок на 8190 байтах, nginx не дає рядку запиту вилізти за буфер із large_client_header_buffers і віддає 414. Ліміт на тіло тим часом вимірюється гігабайтом за замовчуванням. Третій критерій найважливіший для безпеки: усе, що потрапило в адресу, осідає в історії браузера, логах сервера й реферерах, тому секрети й токени їдуть тілом, а токен у query-параметрі — готовий пункт для security-репорту.
Запит відпрацював успішно, але тест падає на розборі JSON. Що сталося?
Найчастіше це відповідь, у якої тіла й не мало бути. Канонічний випадок — 204: операція виконана, показувати нема чого, уся значуща інформація лежить у заголовках, а парсер отримує порожнечу й падає. Те саме дає будь-яка відповідь на HEAD, де відсутність тіла нормативна. Правильна структура коду з цього очевидна: спершу дивимось статус, і лише якщо за контрактом тіло має бути, беремося його розбирати. Падіння в цій точці — не баг сервера, а неправильно написаний асерт.
Як влаштоване тіло multipart/form-data і що в ньому найлегше зіпсувати?
Коли у формі є файл, звичайного кодування форми замало, і тіло розбивають на частини. Кожна частина має власні міні-заголовки, а розділяє їх унікальний рядок-роздільник, оголошений параметром boundary у Content-Type; частина з файлом додатково несе filename і власний Content-Type. Механіка проста, але прискіплива: у тілі роздільник записують як два дефіси плюс саме значення, а в останньому до нього дописують ще пару дефісів у кінець. Унікальність — вимога нормативна: цей рядок не має права зустрітися всередині якоїсь із частин, ані сам по собі, ані на початку іншого рядка; сам параметр для multipart обовʼязковий, має від одного до сімдесяти символів і не може завершуватися пробілом. Тому руками таке тіло не складають: у Playwright чи HTTP-клієнті файл передають обʼєктом і дають бібліотеці зібрати самій — інакше налагоджуватимете не застосунок, а власний роздільник.
Що означає «HTTP — безстановий протокол», якщо в застосунку є сесія?
Безстановий тут саме протокол, а не застосунок. Норма формулюється так: кожен запит має бути зрозумілим сам по собі, і сервер не повинен припускати звʼязок між двома запитами навіть у межах одного зʼєднання; те саме обмеження є в REST, де вся сесія тримається на боці клієнта. Мотив суто прагматичний — коли сервер нікого не памʼятає, будь-яка з десятка однакових машин обробить будь-який запит, і горизонтальне масштабування стає тривіальним. Стан у продукті при цьому нікуди не дівається: зберігається він у базі, а до сервера доїжджає окремо в кожному повідомленні — кукі, які браузер повертає після Set-Cookie, або токеном, який клієнт сам кладе в Authorization. Для тестування це і подарунок, і пастка: ізольований запит легко відтворити й підготувати ним стан замість проходу майстра з десяти екранів, але такої речі, як «увійти один раз на всю сесію», на рівні протоколу немає — автентифікацію везе кожен запит окремо.
Проєкт переїхав на HTTP/2. Що станеться з тестами на методи, коди й заголовки?
Нічого — і саме це головне в питанні. HTTP/2 змінює подання повідомлень, а не їхню семантику: набір методів, і заголовків лишається незмінним, інакшим стає тільки транспортне оформлення. Замість читабельного тексту зʼявляються бінарні кадри, окремого стартового рядка немає взагалі — керівні дані переносять псевдозаголовки з двокрапкою на початку: у запиті це :method, :path, :scheme і :authority, у відповіді — :status. HTTP/3 користується такими самими за призначенням полями й прямо каже про себе, що способу передати номер версії чи пояснювальну фразу зі старого статус-рядка він не визначає. Пастка тут у слові «версія»: HTTP/2 її теж не передає окремим індикатором — специфікація формулює її неявно, «2.0» і для запитів, і для відповідей, — а от пояснювальної фрази вона не згадує жодного разу, тож для HTTP/2 це вивід, а не цитата. Практичних наслідків для QA два: контрактні перевірки методів, кодів і заголовків не переписують, а от асерт, прибитий до тексту пояснювальної фрази, лишається ризиком — і це буде проблема тесту, а не продукту.
Три кейси з робочого життя QA, у яких відповідь дає не код застосунку, а сам запит: чому після мережевого моргання зʼявився другий рахунок, як за пів хвилини прочитати панель мережі по кістках і як перекласти рутину логіну та підготовки даних на заголовки й тіло замість кліків.
Кейс 1. Два замовлення замість одного: хто винен — продукт чи ретрай
Скарга з підтримки: у частини користувачів дублюються замовлення. Відтворити руками не виходить, у логах бекенду видно два повноцінні успішні POST /orders з різницею в кілька секунд і однаковим тілом. Спокуса — завести баг «сервіс створює дублі», але спершу треба відповісти на інше питання: хто надіслав другий запит.
Порядок перевірки простий і майже завжди дає відповідь на другому кроці.
| Крок | Що дивимось | Що це означає |
|---|---|---|
| 1 | метод дубльованого запиту | POST неідемпотентний — повтор законно створює другу сутність, це не поломка сервера |
| 2 | політика ретраїв HTTP-клієнта | автоповтор, увімкнений на всі методи, дає рівно цю картину при таймауті |
| 3 | поведінка кнопки у фронтенді | подвійний клік без блокування — той самий результат іншими руками |
| 4 | наявність механізму захисту від повторів | якщо його немає в контракті, це фіча-запит, а не дефект реалізації |
Далі — те, як цей самий поділ визначає, що можна повторювати в тестах і в клієнті.
| Метод | Повтор після таймауту | Чому |
|---|---|---|
GET, HEAD | безпечно | нічого не змінює, стан сервера той самий |
PUT | безпечно | замінює ресурс цілком, десятий виклик лишає той самий стан, що й перший |
DELETE | безпечно | стан не змінюється, хоч код другої відповіді буде інший |
POST | небезпечно | семантику повтору протокол не визначає — рішення за ресурсом |
PATCH | небезпечно | специфікація ідемпотентності не вимагає, навіть коли тіло однакове |
Три висновки, які варто писати в тікет.
- Дубль після
POST— не автоматично баг сервера. Протокол прямо каже, що повтор може створити другу сутність, тож перед репортом треба назвати джерело другого запиту: клієнт, кнопка чи . - , увімкнений «на всі методи», перетворює мережевий збій на пошкодження даних. Політика повторів має читати метод: ідемпотентні повторювати можна,
POSTіPATCH— ні. - Тест на повторний
DELETEне має чекати однакових кодів.204, потім404— це справна , а не ; треба писати на стан, а не на однаковість відповіді.
Кейс 2. Панель мережі за пів хвилини: сім симптомів і що за ними стоїть
Найдорожчі години в тестуванні втрачаються на баг-репорти про поведінку, яка є нормою протоколу. Ця таблиця — швидкий фільтр перед тим, як заводити тікет.
| Що бачите | Що це насправді | Що робити |
|---|---|---|
OPTIONS перед вашим POST, якого код не слав | CORS preflight: браузер зобовʼязаний спитати дозвіл на непростий крос-оріджин запит | нічого; шукати причину в коді фронтенду марно |
415 у відповідь на валідний JSON | серверу не підходить формат вмісту — дивіться Content-Type, а не тіло | звірити заголовок із контрактом API |
400 замість 415 на кривий Content-Type | відступ від специфікації: для непідтримуваного формату є окремий код | завести тікет на діагностичність, назвавши нормативний код |
у відповіді на HEAD немає Content-Length | сервер має право пропустити поля, що обчислюються при генерації вмісту | прибрати асерт на розмір або брати його з GET |
204 і падіння парсера JSON | тіла немає за означенням, значуще лежить у заголовках | перевіряти статус перед розбором тіла |
401 із WWW-Authenticate | автентифікації вимагає цільовий сервер | дивитися токен і облікові дані застосунку |
407 із Proxy-Authenticate | облікових даних хоче проксі між вами й сервером | дивитися мережеву конфігурацію стенда, а не код автентифікації |
Що з цього варто винести окремо.
- Розрізняти
401і407— це економія годин, а не педантизм. Перший веде в код автентифікації застосунку, другий — у корпоративну мережу; переплутавши, ви шукатимете токен там, де проблема в проксі. - «Порожня відповідь» майже ніколи не є багом сервера.
204і відповідь наHEADтіла не несуть за правилами, тож розбирати його тут нема чого — очікуваний результат пишіть за контрактом, а не за звичкою «прийшов JSON». - Асерт на статусу — міна сповільненої дії. Її дозволено ігнорувати, а в HTTP/3 місця під неї немає взагалі: перевіряйте число.
Кейс 3. Заголовки й тіло як інструмент тесту
Логін через інтерфейс у кожному тесті — найдорожчий спосіб отримати стан, який віддається одним запитом. Токен беруть один раз і підкладають у заголовки, а дані готують замість проходу майстра.
import { test, expect } from '@playwright/test';
test.use({
extraHTTPHeaders: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
Accept: 'application/json',
},
});
test('часткове оновлення не чіпає непередані поля', async ({ request }) => {
const before = await (await request.get('/api/users/42')).json();
// PATCH везе інструкцію змінити одне поле, а не нове подання ресурсу
const res = await request.patch('/api/users/42', { data: { role: 'admin' } });
expect(res.status()).toBe(200);
const after = await (await request.get('/api/users/42')).json();
expect(after.role).toBe('admin');
expect(after.email, 'PATCH не мав чіпати email').toBe(before.email);
});
Дзеркальний негативний сценарій показує головний повного заміщення: PUT без частини полів законно затирає те, чого ви не передали.
test('неповний PUT затирає непередані поля', async ({ request }) => {
await request.put('/api/users/42', { data: { role: 'admin' } });
const after = await (await request.get('/api/users/42')).json();
// якщо контракт вимагає цілісне подання — email тут уже порожній
expect(after.email, 'контракт: PUT замінює ресурс цілком').toBeFalsy();
});
Файл у формі передають обʼєктом, а не складеним руками тілом: бібліотека сама поставить роздільник, filename і тип частини.
test('завантаження звіту приймає PDF', async ({ request }) => {
const res = await request.post('/api/upload', {
multipart: {
title: 'Some report name',
file: {
name: 'report.pdf',
mimeType: 'application/pdf',
buffer: Buffer.from('%PDF-1.4 ...'),
},
},
});
expect(res.status()).toBe(201);
});
А ось спроба, яка виглядає робочою й тихо нічого не робить.
// НЕ спрацює: Cookie у переліку forbidden request-header —
// підміну проігнорують, запит поїде з реальними кукі, тест зеленітиме дарма
await page.route('**/api/**', (route) =>
route.continue({
headers: { ...route.request().headers(), cookie: 'role=admin' },
}),
);
Що дивитися і чому:
- Токен у заголовку економить не хвилини, а стабільність. Кожен прохід логіну через інтерфейс додає тесту чужі точки — рендер, анімації, , — жодна з яких до предмета перевірки не має стосунку.
- Різницю
PUTіPATCHперевіряють станом після запиту, а не кодом відповіді.200прийде в обох випадках; втраченийemailвидно лише наступнимGET. - Заборонені заголовки не сигналізують про помилку. Підміна
Cookie,Host,OriginчиRefererмовчки не застосовується — тому кожен сценарій із підміною потребує асерту, який доводить, що нове значення справді доїхало. - Тіло
multipartруками не складають. Роздільник має бути унікальним і не траплятися всередині жодної частини; зламавши його, ви налагоджуватимете власний замість застосунку. - Секрет у тілі, а не в адресі. Той самий токен, переданий query-параметром, осяде в історії браузера, логах сервера й реферерах — і це вже не зручність тесту, а знахідка для security-репорту.
Будова повідомлення
- Можу назвати чотири частини по порядку, показати, що схема однакова для запиту й відповіді, і пояснити, чому порожній рядок — елемент синтаксису, а не форматування.
- Розумію, чому число статусу, а не поруч: клієнту рекомендовано на неї не спиратися, а в HTTP/3 місця під неї немає взагалі.
Семантика методів
- Можу перелічити три наскрізні властивості методу — безпечність, , кешованість — і показати, що осі не збігаються:
PUTідемпотентний, але ні безпечний, ні кешований. - Знаю точне означення («лише читання» з погляду клієнта), чому «без побічних ефектів» — спрощення, і навіщо цей поділ узагалі існує: заради і .
- Розумію, що протокол фіксує намір, а не гарантує поведінку, і тому видалення, повішене на
GET, — дефект контракту, а не місцева особливість реалізації. - Знаю різницю
PUTvsPATCHза вмістом тіла (готове подання проти набору інструкцій), памʼятаю затирання непереданих полів і те, що формат тілаPATCHодним стандартом не закріплений — дивлюсь контракт. - Тримаю в голові пастку
HEAD: тіла у відповіді немає нормативно, аContent-Lengthсервер має право не віддати, тож перевірка розміру без переконання, що поле прийшло, дає плаваюче падіння.
Ідемпотентність і ретраї
- Знаю означення через стан сервера, перелік і неідемпотентну трійку
POST,PATCH,CONNECT. - Не плутаю стан сервера з відповіддю:
404на повторнийDELETEідемпотентність не порушує, і тест має його очікувати. - Розумію, чому однакове тіло не робить
PATCHідемпотентним, вмикаю за методом, а побачивши дублі — спершу дивлюсь налаштування повторів у самому HTTP-клієнті, і лише потім підозрюю продукт.
Заголовки
- Знаю різницю
Content-TypevsAcceptза напрямком: перший описує вміст, який їде зараз, другий — вигляд, у якому клієнт чекає майбутню відповідь. - Памʼятаю нормативний код на непідтримуваний формат тіла —
415, — і називаю400на цьому місці відступом, який варто згадати в баг-репорті. - Розумію ваги
qв узгодженні вмісту: діапазон від нуля до одиниці, відсутність ваги дорівнює одиниці,q=0— , а не низький пріоритет. - Знаю про forbidden request-header і головний симптом: підміна
Cookie,Host,OriginчиRefererмовчки ігнорується, тест зеленіє даремно, аUser-Agentпідміняється саме тому, що в переліку його немає. - Можу пояснити, чому
Basicбез TLS не захист ( — кодування, роздільником пари є перша двокрапка), і не плутаю401ізWWW-Authenticateта407ізProxy-Authenticate: друга пара веде в мережеву конфігурацію стенда, а не в код застосунку.
Тіло, query і multipart
- Можу назвати запити з тілом (
POST,PUT,PATCH) і відповіді без нього (204, відповідь наHEAD) — і перевіряю статус до того, як братися розбирати тіло. - Обираю між query і тілом за трьома критеріями — структура, видимість, ліміт довжини, — памʼятаю порядок величин (близько 8 кБ на рядок запиту проти гігабайта на тіло за замовчуванням) і не пускаю секрети в адресу.
- Можу описати склад
multipart/form-data: частини з власними заголовками, унікальнийboundary,filenameі тип у частині з файлом — і не складаю таке тіло руками.
Безстановість і версії протоколу
- Розділяю безстановий протокол і застосунок зі станом, знаю, чим стан приносять (кукі або токен), і памʼятаю, що «увійти один раз на всю сесію» на рівні протоколу не існує.
- Знаю, що HTTP/2 змінює подання, а не семантику: псевдозаголовки замість стартового рядка, ті самі методи й коди — тому контрактні тести переїзд не переписує.
Квіз
Перед стартом
- Питань: 14
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Що входить у стартовий рядок запиту (request-line)?
