HTTP статус-коди
Зміст
Кожна відповідь сервера на HTTP-запит починається з трицифрового числа — статус-коду (status code). Це найкоротше можливе повідомлення про долю запиту: одним числом сервер каже, чи вдалося його опрацювати, а якщо ні — то на чиєму боці проблема і що з нею робити далі. Червоний тест, флак (flakiness) у пайплайні, «нічого не працює на стейджі» — усе це починається з питання «а який код повернув сервер?». Тому для AQA-інженера читання статус-кодів — базовий інструмент діагностики, а не формальна деталь відповіді.
Анатомія відповіді
Спершу подивімося, де взагалі живе статус-код. Ось сирий (raw) обмін даними за протоколом HTTP:
GET /api/users/42 HTTP/1.1
Host: example.com
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 53
{"id": 42, "name": "Ada", "email": "ada@example.com"}
Перший рядок відповіді — стартовий рядок статусу (status line): версія протоколу, числовий код і коротка текстова фраза (reason phrase, тут OK). Машина орієнтується на число, а не на фразу. Текст OK чи Not Found призначений для людини: він може відрізнятися між серверами, а в HTTP/2 і HTTP/3 його в протоколі вже немає взагалі — передається лише код. Тому в тесті перевіряй response.status() === 200, а не текстовий рядок.
Усі коди поділені на пʼять класів за першою цифрою. Це не косметика: перша цифра одразу задає стратегію реакції.
| Клас | Значення | Хто «власник» ситуації | Що з цим робити |
|---|---|---|---|
| 1xx | Інформаційні (informational) | — | Проміжні, рідко видно вручну |
| 2xx | Успіх (success) | — | Запит опрацьовано, читай тіло/заголовки |
| 3xx | Перенаправлення (redirection) | клієнт | Треба зробити ще один крок (перейти, взяти з кешу) |
| 4xx | Помилка клієнта (client error) | клієнт | Запит неправильний — виправляй запит |
| 5xx | Помилка сервера (server error) | сервер | Запит нормальний, зламався сервер |
Ключова лінія розлому — між 4xx і 5xx. 4xx означає «ти (клієнт) зробив щось не так»; 5xx означає «я (сервер) не впорався». Ця межа — головний орієнтир, коли зʼясовуєш, у чиєму коді баг; до неї ми ще повернемося окремо.
1xx (наприклад, 100 Continue, 101 Switching Protocols) на практиці AQA бачить рідко — це технічні проміжні сигнали, які клієнтська бібліотека опрацьовує сама. Далі зосередимося на решті чотирьох класів.
2xx — успіх
Успіх теж буває різним, і плутати його різновиди — типова помилка в тестах.
| Код | Назва | Коли | Чи є тіло відповіді |
|---|---|---|---|
| 200 | OK | Загальний успіх (частіше GET) | Так, з даними |
| 201 | Created | Створено новий ресурс (частіше POST) | Зазвичай так + заголовок Location |
| 204 | No Content | Успіх, але показувати нема чого | Ні, тіло порожнє |
200 vs 201 vs 204
200 OK — універсальний успіх. Сервер опрацював запит і повертає результат у тілі. Найчастіше — відповідь на GET.
201 Created каже більше, ніж просто «ок»: створено новий ресурс. За домовленістю (convention) сервер додає заголовок Location з адресою нового ресурсу, а часто і його представлення в тілі.
POST /api/users HTTP/1.1
Content-Type: application/json
{"name": "Grace", "email": "grace@example.com"}
HTTP/1.1 201 Created
Location: /api/users/43
Content-Type: application/json
{"id": 43, "name": "Grace", "email": "grace@example.com"}
204 No Content — успіх без тіла. Класичний випадок: DELETE, який видалив запис, або PUT, який оновив, коли повертати нема чого. Тіло порожнє, тому спроба розпарсити з відповіді 204 JSON впаде.
Чому це важить для автотестів. Уяви асерт expect(res.status).toBe(200) на ендпоінті, який після рефакторингу почав чесно повертати 201 на створення. Тест червоніє, хоча функціонал працює. І навпаки: якщо ендпоінт створення повертає 200 замість 201, це може бути легітимна претензія до API-контракту — тест на статус тут перевіряє саме контракт, а не «щось повернулося». Ще типова пастка: код читає await res.json() після 204 і падає з помилкою парсингу — тест «червоний», але справжня причина в тому, що тіла там і не мало бути.
3xx — перенаправлення
3xx каже клієнту: «сам по собі запит нормальний, але щоб дійти до мети, зроби ще крок». Найчастіше цей крок — піти за іншою адресою із заголовка Location.
301 vs 302 і оглядово 307/308
Тут головне питання — постійність і збереження методу.
| Код | Назва | Постійність | Чи зберігає метод/тіло |
|---|---|---|---|
| 301 | Moved Permanently | Постійно | Історично метод міг змінитися (POST→GET) |
| 302 | Found | Тимчасово | Історично метод міг змінитися (POST→GET) |
| 307 | Temporary Redirect | Тимчасово | Метод і тіло зберігаються |
| 308 | Permanent Redirect | Постійно | Метод і тіло зберігаються |
301 — ресурс переїхав назавжди; клієнти й пошуковики можуть закешувати новий шлях і надалі ходити одразу туди. 302 — переїзд тимчасовий, оригінальна адреса лишається канонічною.
Історична пастка 301/302: специфікація не вимагала змінювати метод при перенаправленні, але браузери на практиці перетворювали POST на GET (і губили тіло). RFC 9110 прямо визнає це «з історичних причин»: клієнт МОЖЕ замінити метод із POST на GET при 301/302. Саме тому зʼявилися явні коди без цієї двозначності: 307 Temporary Redirect і 308 Permanent Redirect зберігають і метод, і тіло запиту. 308 додано пізніше окремим документом (RFC 7538, 2015) як «постійний брат» 307. Просте правило: якщо треба перенаправити POST/PUT/DELETE без втрати тіла — це 307/308, а не 301/302.
Що це означає для тестів. Браузери й більшість HTTP-клієнтів за замовчуванням автоматично йдуть за перенаправленнями (follow redirects) — помітний виняток curl, який без прапорця -L за редіректом не піде. Тому в коді ти часто бачиш уже фінальний 200, навіть якщо між ним був 301. Це і зручно, і небезпечно:
- Якщо треба перевірити сам факт редіректу (наприклад,
http→httpsабо старий URL→новий), автопрямування треба вимкнути й асертити проміжний код іLocation.
// Playwright APIRequestContext: не йти за редіректом, перевірити його явно
const res = await request.get('/old-path', { maxRedirects: 0 });
expect(res.status()).toBe(301);
expect(res.headers()['location']).toBe('/new-path');
- У UI-тестах ланцюжок редіректів (наприклад, редірект на сторінку логіну і назад) — часте джерело флаку: очікування (wait) чекає на URL, який зʼявляється лише на мить. Надійніше чекати на фінальний стан сторінки, а не на проміжний URL.
Побіжно: є ще 303 See Other, який навпаки — навмисно змушує клієнта зробити GET після, наприклад, обробки форми. Але для базового рівня достатньо запамʼятати чотири коди з таблиці вище.
304 і кеш
304 Not Modified формально належить до 3xx, але за суттю це не «піди в інше місце», а «нічого не змінилося, бери своє з кешу». Це серце умовних запитів (conditional requests) — механізму, який економить трафік.
Працює це так. Коли сервер віддає ресурс, він додає «валідатор» (validator): або ETag — унікальний відбиток вмісту, або Last-Modified — дату останньої зміни.
HTTP/1.1 200 OK
ETag: "a1b2c3"
Last-Modified: Tue, 07 Jul 2026 10:00:00 GMT
Cache-Control: no-cache
...тіло...
Наступного разу клієнт питає умовно: «віддай, тільки якщо змінилося з часів цього відбитка».
GET /api/config HTTP/1.1
If-None-Match: "a1b2c3"
If-Modified-Since: Tue, 07 Jul 2026 10:00:00 GMT
Якщо на сервері нічого не змінилося, він відповідає:
HTTP/1.1 304 Not Modified
ETag: "a1b2c3"
304 не містить тіла — воно й не потрібне, бо актуальна копія вже є у клієнта. Заголовок If-None-Match працює в парі з ETag, а If-Modified-Since — з Last-Modified.
Чому це болить в автотестах. Кешування — прихована причина «стану, що протікає» між тестами. Тест A завантажив сторінку й наповнив кеш; тест B бачить 304 і старі дані, хоча очікує свіжі. Особливо підступно це в API-тестах, де той самий клієнтський контекст (context) переживає кілька кейсів. Тому для ізоляції стану (state isolation) кеш часто примусово вимикають: чистий контекст на тест, заголовок Cache-Control: no-cache у запитах або відключення кешу браузера. Якщо тест то зелений, то червоний залежно від порядку запуску, — 304 і кеш є першим підозрюваним.
4xx — помилка клієнта
4xx означає: сервер зрозумів, що ти від нього хочеш, і свідомо відмовив, бо із запитом щось не так. Виправляти треба запит (або дані/права клієнта), а не сервер.
400 Bad Request
400 — запит зламаний на рівні синтаксису чи форми: невалідний JSON, відсутній обовʼязковий параметр, який неможливо навіть розібрати, некоректний формат. Сервер не може навіть почати осмислено його опрацьовувати.
POST /api/users HTTP/1.1
Content-Type: application/json
{"name": "Grace", "email": // обірваний JSON
HTTP/1.1 400 Bad Request
401 vs 403
Ці два плутають найчастіше, а різниця принципова.
| 401 Unauthorized | 403 Forbidden | |
|---|---|---|
| Питання | «Хто ти?» | «Тобі можна?» |
| Суть | Не автентифіковано (authentication) | Автентифіковано, але немає прав (authorization) |
| Типова причина | Немає/протух/невалідний токен | Валідний токен, але роль не має доступу |
| Реакція клієнта | Залогінься / онови токен | Логін не допоможе, потрібні інші права |
401 — сервер не знає, хто ти: токен відсутній, прострочений або підроблений. За специфікацією (RFC 9110) відповідь 401 МУСИТЬ містити заголовок WWW-Authenticate, який описує, як автентифікуватися; на практиці цю вимогу часто порушують. 403 — сервер тебе впізнав, але цьому користувачеві сюди не можна: звичайний користувач лізе в адмінку. Повторний логін тут нічого не змінить.
Плутанина 401/403 — це не педантизм. Уяви автотест ролей: він логіниться звичайним юзером і перевіряє, що адмін-ендпоінт недоступний. Якщо асертити 401, тест бреше — бо юзер якраз автентифікований, і правильна відмова тут 403. А 401 замість 403 (чи навпаки) на реальному ендпоінті — це вже знахідка (bug), яку тест на статус і має зловити.
Ще одна тонкість: частина реальних API навмисно повертає 404 замість 403 на закриті ресурси, щоб не розкривати сам факт їхнього існування (поширений приклад — приватні ресурси, які для стороннього виглядають як неіснуючі). Тому перед написанням асерту звіряйся з контрактом конкретного API.
404 Not Found
404 — ресурсу за цією адресою немає. Або ніколи не було, або видалено, або URL зібрано неправильно. Для AQA 404 підступний тим, що маскує кілька різних причин: реальну відсутність даних, помилку в шляху ендпоінта в тесті (одрук у слагу, неправильний ID) або те, що фікстура (fixture) не створила потрібний запис. Коли автотест несподівано отримує 404, перше питання — «а той запис/маршрут узагалі існує в цьому середовищі?».
409 Conflict
409 — запит валідний, але конфліктує з поточним станом ресурсу. Класика: реєстрація на вже зайнятий email, спроба створити дублікат унікального запису, конфлікт одночасного редагування (хтось змінив ресурс, поки ти правив свою версію).
POST /api/users HTTP/1.1
Content-Type: application/json
{"email": "ada@example.com"}
HTTP/1.1 409 Conflict
Content-Type: application/json
{"error": "email already registered"}
409 — прямий сигнал про проблему ізоляції стану в тестах. Якщо тест створює юзера з фіксованим email і при повторному запуску отримує 409, значить, попередній прогін не прибрав за собою. Лікування — унікальні дані на кожен прогін (email з таймстемпом чи UUID) та надійне прибирання (teardown/cleanup).
422 vs 400
Ця пара тонша за 401/403, і саме її люблять питати на співбесідах.
400 Bad Request— запит зламаний на рівні синтаксису/форми: сервер не може його навіть розібрати.422 Unprocessable Content— запит синтаксично коректний (валідний JSON, усі поля на місці), сервер його розібрав, але вміст не проходить бізнес-валідацію: вік відʼємний, дата в минулому там, де має бути майбутнє, email у правильному форматі, але порожній рядок.
Простими словами: 400 — «я не зрозумів, що ти написав»; 422 — «я зрозумів, але так не можна».
POST /api/users HTTP/1.1
Content-Type: application/json
{"name": "Grace", "email": "grace@example.com", "age": -5}
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{"errors": {"age": ["must be greater than 0"]}}
Історична деталь: 422 прийшов зі світу WebDAV (RFC 4918) і довго був там «чужим». Тепер його внесено до сучасної специфікації HTTP (RFC 9110), заодно перейменувавши з Unprocessable Entity на Unprocessable Content. Обидві назви ще трапляються в документації й бібліотеках.
Практична складність: різні фреймворки поводяться по-різному. Одні на помилку валідації віддають 422, інші — 400. Тому в тесті на негативний сценарій (відʼємний вік) не варто наосліп асертити 400 — треба звірятися з реальною поведінкою конкретного API. І навпаки: якщо API документує 422 на валідацію, а повертає 400 (або взагалі 500), це знахідка.
429 Too Many Requests
429 — обмеження частоти запитів (rate limiting): клієнт зробив забагато звернень за проміжок часу. Відповідь може містити заголовок Retry-After, який каже, скільки чекати перед наступною спробою (значення — або невідʼємне число секунд, або конкретна HTTP-дата). Код визначено в RFC 6585.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{"error": "rate limit exceeded"}
Для AQA 429 — одне з найпідступніших джерел флаку в CI. Кілька паралельних воркерів (workers) одночасно гатять в один ендпоінт, спрацьовує ліміт — і частина тестів червоніє «випадково». Симптом упізнаваний: локально все зелене, а в пайплайні з паралелізмом — плавучі падіння. Рецепти: поважати Retry-After у ретраях, зменшити паралелізм на «гарячих» ендпоінтах, а в чистих UI-тестах перехоплювати мережу (network interception) і мокати ліміт, якщо мета тесту не в самому rate-limiting.
5xx — помилка сервера
5xx означає протилежне до 4xx: запит був нормальний, а зламався сам сервер. Клієнт нічого не зробив не так (принаймні формально). Розрізняти підкласи важливо, бо вони вказують на різні місця поломки в інфраструктурі.
| Код | Назва | Де ламається | Типова причина |
|---|---|---|---|
| 500 | Internal Server Error | У самому застосунку | Неопрацьований виняток (exception), падіння коду |
| 502 | Bad Gateway | На шлюзі/проксі | Апстрім (upstream) віддав некоректну/поламану відповідь |
| 503 | Service Unavailable | У застосунку | Сервіс тимчасово не тягне: перевантаження, деплой, обслуговування |
| 504 | Gateway Timeout | На шлюзі/проксі | Апстрім не відповів вчасно |
Щоб відчути різницю між 502/503/504, уяви типову топологію: браузер → зворотний проксі/балансувальник (reverse proxy, наприклад nginx) → застосунок → база даних.
500— застосунок дійшов до твого запиту, почав його виконувати й впав з необробленою помилкою в коді. Це «щось пішло не так усередині», найзагальніший серверний код.502— проксі звернувся до застосунку (свого апстріму), а той віддав щось нечитабельне або взагалі впав/не піднявся. Проксі стоїть, апстрім — ні.503— сам сервіс свідомо каже «зараз не можу»: перевантажений, на технічному обслуговуванні або в процесі деплою. Часто супроводжуєтьсяRetry-After. На відміну від500, це радше «зайди пізніше», ніж «я зламався».504— проксі почекав на відповідь апстріму й не дочекався у відведений час. Апстрім, імовірно, живий, але надто повільний (важкий запит до БД, завислий зовнішній виклик).
Практичний висновок для тестів: 503/504 під час прогону часто означають не баг у продукті, а стан середовища — деплой у процесі, стейдж «прогрівається», база під навантаженням. Зустрів масові 502/503/504 одразу після релізу на середовище — спершу переконайся, що середовище взагалі піднялося, а не пиши баг на продукт.
Діагностика: «отримав 500 — це баг фронта, бекенда чи тесту?»
Це питання ставлять на кожній співбесіді, і в реальній роботі теж. Плутанина в тому, що 5xx завжди повертає сервер, але спричинити його може будь-яка ланка. Ось як міркувати системно.
Спершу — базовий принцип. 5xx означає, що сервер не впорався з опрацюванням запиту. Це майже завжди вказує щонайменше на одну проблему на бекенді: навіть якщо клієнт надіслав відверте сміття, добре спроєктований бекенд має відповісти 400/422, а не впасти з 500. Тобто «неопрацьований 500 на кривому вводі» — це вже щонайменше півбага бекенда (пропущена валідація). Але це не вся картина: тригер міг прийти ззовні. Тому діагностику веди по кроках.
Крок 1. Подивись на сам запит, що спричинив 500. Це відсікає найбільше версій одразу.
- URL, метод, заголовки (особливо
AuthorizationіContent-Type), тіло запиту. Чи взагалі запит валідний? - Якщо запит криво зібраний (немає обовʼязкового поля, зламаний JSON, не той
Content-Type) — імовірний тригер на боці клієнта: фронтенду або тесту. Але зафіксуй і бекенд-складову: на такий ввід мала бути відповідь4xx, а не500.
Крок 2. Відтвори вручну (reproduce). Повтори точно той самий запит поза тестом — через curl чи Postman.
curl -i -X POST https://staging.example.com/api/users \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"name":"Grace","email":"grace@example.com"}'
- Відтворюється тим самим валідним запитом → проблема стабільна й на боці сервера. Це кандидат на баг бекенда.
- Не відтворюється, а тест усе одно падає → підозра зміщується на тест: можливо, він шле не те, що ти думаєш (не підставилась змінна, протух токен, крива фікстура, неправильне середовище).
Крок 3. Заглянь у логи сервера. 500 майже завжди лишає стектрейс (stack trace) у логах застосунку. Він прямо показує, у якому рядку коду і на чому впало. Це найшвидший спосіб перевести розмову з «здається» на «ось конкретне місце». Дисципліна тут проста: без вимірюваного доказу не заявляй «root cause» — стектрейс і є той доказ.
Крок 4. Відділи середовище від продукту. Чи це 500, чи насправді 502/503/504? Чи піднялося середовище? Чи не йде деплой просто зараз? Масові однотипні 5xx — частіше про інфраструктуру, ніж про логіку.
Зведена таблиця для швидкого висновку:
| Що бачиш | Найімовірніший власник проблеми |
|---|---|
Запит валідний, 500 відтворюється вручну, у логах стектрейс | Баг бекенда |
Запит невалідний, але бекенд падає з 500 (а не 4xx) | Баг бекенда (пропущена валідація) + кривий ввід від клієнта/тесту |
Запит невалідний, 500 не відтворюється чистим запитом | Баг тесту (шле не те) |
502/503/504, часто масові, привʼязані до деплою/навантаження | Середовище/інфраструктура, не продукт |
500 лише в CI під паралелізмом, поодинці зелено | Ізоляція стану / гонки (races) / rate limit — розкопуй тест і дані |
Головна дисципліна: не заявляй причину, доки не перевірив. Спершу подивись запит, відтвори, прочитай логи — і лише тоді став діагноз. Формулювання «схоже на баг бекенда, стектрейс указує на UsersController#create» коштує рівно стільки, скільки під ним доказів; «напевно фронт винен» без відтворення не коштує нічого.
Що таке HTTP статус-код і на які класи поділяються коди?
Статус-код — трицифрове число в першому рядку відповіді сервера, яке повідомляє долю запиту: чи опрацьовано його, а якщо ні — на чиєму боці проблема. Коди поділені на пʼять класів за першою цифрою: 1xx — інформаційні проміжні сигнали, 2xx — успіх, 3xx — перенаправлення (потрібен ще один крок), 4xx — помилка клієнта, 5xx — помилка сервера. Перша цифра — не косметика: вона одразу задає стратегію реакції. Побачив 4xx — виправляй запит; побачив 5xx — запит нормальний, зламався сервер. Для AQA це базовий інструмент діагностики: розбір будь-якого червоного тесту чи «нічого не працює на стейджі» починається з питання «а який код повернув сервер?».
У чому принципова різниця між 4xx і 5xx?
Різниця у «власнику» проблеми: 4xx — «ти (клієнт) зробив щось не так», 5xx — «я (сервер) не впорався». При 4xx сервер зрозумів запит і свідомо відмовив, бо із самим запитом щось не так — виправляти треба запит, дані або права клієнта. При 5xx запит був нормальний, а зламався сервер: неопрацьований виняток, перевантаження, проблеми на проксі. Ця межа — головний орієнтир, коли зʼясовуєш, у чиєму коді баг. У тестуванні вона визначає, куди заводити дефект: 4xx на валідному запиті — питання до клієнта чи самого тесту, 5xx — майже завжди щонайменше одна проблема на бекенді, бо добре спроєктований сервер навіть на сміття має відповідати 400/422, а не падати.
Чим відрізняються 200, 201 і 204?
Це три різновиди успіху. 200 OK — універсальний успіх з результатом у тілі, найчастіше відповідь на GET. 201 Created каже більше: створено новий ресурс; за домовленістю сервер додає заголовок Location з адресою нового ресурсу, а часто і його представлення в тілі — типова відповідь на POST. 204 No Content — успіх без тіла: класика для DELETE, який видалив запис, або PUT, коли повертати нема чого. Для автотестів різниця практична: асерт expect(res.status).toBe(200) почервоніє, якщо ендпоінт після рефакторингу почав чесно повертати 201, хоча функціонал працює. І навпаки — 200 замість 201 на створенні може бути легітимною претензією до API-контракту. Ще типова пастка: await res.json() після 204 падає з помилкою парсингу, бо тіла там і не мало бути.
У чому різниця між 401 Unauthorized і 403 Forbidden?
401 відповідає на питання «хто ти?», 403 — «тобі можна?». 401 — сервер не знає, хто ти: токен відсутній, прострочений або підроблений; це проблема автентифікації (authentication), і реакція клієнта — залогінитися чи оновити токен. 403 — сервер тебе впізнав, але цьому користувачеві сюди не можна: звичайний юзер лізе в адмінку; це проблема авторизації (authorization), і повторний логін нічого не змінить. За специфікацією (RFC 9110) відповідь 401 мусить містити заголовок WWW-Authenticate, хоча на практиці цю вимогу часто порушують. Для AQA різниця не педантизм: у тесті ролей, де залогінений юзер перевіряє недоступність адмін-ендпоінта, правильна відмова — 403, і асерт на 401 тут бреше. А 401 замість 403 (чи навпаки) на реальному ендпоінті — це вже знахідка, яку тест на статус і має зловити.
Автотест несподівано отримав 404. Які версії треба перевірити перед тим, як писати баг?
404 означає, що ресурсу за цією адресою немає, але це маскує кілька різних причин. Або даних справді немає (ніколи не було чи видалено), або URL зібрано неправильно — одрук у слагу, неправильний ID, не те середовище, — або фікстура не створила потрібний запис перед тестом. Тому перше питання при несподіваному 404 — «а той запис чи маршрут узагалі існує в цьому середовищі?». Перевіряється це просто: повторити запит вручну з явним ID і подивитися, що реально підставилось у URL тесту. Часто виявляється, що баг у тесті чи в підготовці даних, а не в продукті.
Чому в тесті треба перевіряти числовий код, а не текст «OK» чи «Not Found»?
Машина орієнтується на число, а текстова фраза (reason phrase) призначена для людини. Фраза може відрізнятися між серверами, а в HTTP/2 і HTTP/3 її в протоколі немає взагалі — передається лише код. Тобто асерт на текст ламається від зміни сервера чи версії протоколу, хоча поведінка API не змінилась. Тому в тесті правильно перевіряти response.status() === 200, а не текстовий рядок статусу.
Чим відрізняються 301 і 302, і навіщо придумали 307 і 308?
301 Moved Permanently — ресурс переїхав назавжди, клієнти й пошуковики можуть закешувати новий шлях; 302 Found — переїзд тимчасовий, канонічною лишається стара адреса. Історична пастка обох: браузери на практиці перетворювали POST на GET при перенаправленні й губили тіло запиту, і RFC 9110 прямо визнає, що клієнт може так робити «з історичних причин». Саме тому зʼявилися явні коди без двозначності: 307 Temporary Redirect і 308 Permanent Redirect зберігають і метод, і тіло (308 додано пізніше окремим RFC 7538 як «постійний брат» 307). Просте правило: треба перенаправити POST/PUT/DELETE без втрати тіла — це 307/308, а не 301/302. Побіжно існує ще 303 See Other, який навпаки навмисно змушує клієнта зробити GET — наприклад, після обробки форми.
На старому URL налаштований редірект, але тест бачить 200. Чому, і як перевірити сам факт редіректу?
Браузери й більшість HTTP-клієнтів за замовчуванням автоматично йдуть за перенаправленнями (follow redirects), тому в коді видно вже фінальний 200, навіть якщо між ним був 301 — помітний виняток curl, який без прапорця -L за редіректом не піде. Це зручно для звичайних тестів, але ховає сам редірект. Щоб перевірити його явно (наприклад, http→https чи старий URL→новий), автопрямування треба вимкнути й асертити проміжний код разом із заголовком Location: у Playwright APIRequestContext це maxRedirects: 0, після чого expect(res.status()).toBe(301) і перевірка res.headers()['location']. Окремий практичний нюанс: у UI-тестах ланцюжок редіректів — часте джерело флаку, бо очікування чекає на URL, який зʼявляється лише на мить; надійніше чекати на фінальний стан сторінки, а не на проміжний URL.
Що означає 409 Conflict і про що він сигналить, якщо зʼявляється при повторних запусках тестів?
409 — запит валідний, але конфліктує з поточним станом ресурсу: реєстрація на вже зайнятий email, дублікат унікального запису, конфлікт одночасного редагування. У контексті автотестів це прямий сигнал про проблему ізоляції стану: якщо тест створює юзера з фіксованим email і на повторному прогоні отримує 409, значить, попередній прогін не прибрав за собою. Лікування стандартне — унікальні дані на кожен прогін (email з таймстемпом чи UUID) плюс надійне прибирання (teardown/cleanup). Тобто 409 у CI частіше вказує на дизайн тестових даних, ніж на баг продукту — але на реальному флоу реєстрації це якраз очікувана й правильна відповідь, яку варто покрити окремим негативним тестом.
У чому різниця між 400 Bad Request і 422 Unprocessable Content?
400 — запит зламаний на рівні синтаксису чи форми: невалідний JSON, некоректний формат — сервер не може його навіть розібрати. 422 — запит синтаксично коректний, сервер його розібрав, але вміст не проходить бізнес-валідацію: вік відʼємний, дата в минулому там, де має бути майбутнє. Простими словами: 400 — «я не зрозумів, що ти написав», 422 — «я зрозумів, але так не можна». Історична деталь: 422 прийшов зі світу WebDAV (RFC 4918), тепер внесений до RFC 9110 і перейменований з Unprocessable Entity на Unprocessable Content — обидві назви ще трапляються в документації. Практичний наслідок для тестів: фреймворки поводяться по-різному, одні на помилку валідації віддають 422, інші 400, тому в негативному сценарії не варто наосліп асертити 400 — треба звірятися з контрактом конкретного API. І навпаки: якщо API документує 422, а повертає 400 чи взагалі 500 — це знахідка.
Що таке 429 Too Many Requests і чому тести з ним падають лише в CI?
429 — обмеження частоти запитів (rate limiting): клієнт зробив забагато звернень за проміжок часу; код визначено в RFC 6585. Відповідь може містити заголовок Retry-After — скільки чекати перед наступною спробою (число секунд або конкретна HTTP-дата). Для AQA це одне з найпідступніших джерел флаку: кілька паралельних воркерів у CI одночасно гатять в один ендпоінт, спрацьовує ліміт — і частина тестів червоніє «випадково». Симптом упізнаваний: локально все зелене, а в пайплайні з паралелізмом — плавучі падіння. Рецепти: поважати Retry-After у ретраях, зменшити паралелізм на «гарячих» ендпоінтах, а в чистих UI-тестах — перехоплювати мережу й мокати ліміт, якщо мета тесту не в самому rate-limiting.
Як працює 304 Not Modified і механізм умовних запитів?
304 формально належить до 3xx, але за суттю це не «піди в інше місце», а «нічого не змінилося, бери своє з кешу» — серце умовних запитів (conditional requests), які економлять трафік. Механізм: віддаючи ресурс, сервер додає валідатор — ETag (унікальний відбиток вмісту) або Last-Modified (дату останньої зміни). Наступного разу клієнт питає умовно: If-None-Match з ETag-ом або If-Modified-Since з датою — «віддай, тільки якщо змінилося». Якщо не змінилося, сервер відповідає 304 без тіла — воно й не потрібне, бо актуальна копія вже є у клієнта. Пари жорсткі: If-None-Match працює з ETag, If-Modified-Since — з Last-Modified. У тестах про це варто памʼятати, коли асертиш тіло відповіді: після 304 тіла немає, дані беруться з кешу клієнта.
Тест зелений поодинці, але червоний у повному наборі. До чого тут 304 і кеш?
Кешування — прихована причина «стану, що протікає» між тестами. Сценарій: тест A завантажив сторінку й наповнив кеш; тест B робить той самий запит, отримує 304 і старі дані, хоча очікує свіжі після своїх змін. Особливо підступно це в API-тестах, де один клієнтський контекст переживає кілька кейсів. Симптом класичний: результат залежить від порядку запуску тестів — тоді 304 і кеш перший підозрюваний. Лікування — примусова ізоляція стану: чистий контекст на кожен тест, заголовок Cache-Control: no-cache у запитах або відключення кешу браузера.
Чим відрізняються 500, 502, 503 і 504?
Усі чотири — серверні помилки, але вказують на різні місця поломки в топології «браузер → проксі/балансувальник → застосунок → база». 500 Internal Server Error — застосунок дійшов до запиту й впав з необробленим винятком: найзагальніший код, «щось пішло не так усередині». 502 Bad Gateway — проксі звернувся до свого апстріму (upstream), а той віддав щось нечитабельне або взагалі не піднявся: проксі стоїть, апстрім — ні. 503 Service Unavailable — сервіс свідомо каже «зараз не можу»: перевантаження, деплой, обслуговування; часто з Retry-After, і це радше «зайди пізніше», ніж «я зламався». 504 Gateway Timeout — проксі не дочекався відповіді апстріму у відведений час: апстрім, імовірно, живий, але надто повільний (важкий запит до БД, завислий зовнішній виклик). Практичний висновок для прогонів: масові 502/503/504 одразу після релізу — частіше стан середовища (деплой у процесі, стейдж прогрівається), ніж баг продукту; спершу переконайся, що середовище взагалі піднялося, а вже потім заводь дефект.
API повертає 404 замість 403 на ресурс, до якого немає прав. Це баг?
Не обовʼязково — частина реальних API робить це навмисно, щоб не розкривати сам факт існування закритого ресурсу: для стороннього приватний ресурс виглядає як неіснуючий. Тобто формально «неправильний» код може бути свідомим рішенням контракту з міркувань безпеки. Наслідок для тестування: перед написанням асерту на код відмови треба звірятися з контрактом конкретного API, а не з «як має бути за підручником». Якщо контракт документує 404 на чужі ресурси — тест асертить 404; якщо контракт каже 403, а приходить 404 — це вже привід для розмови з командою. Сильна відповідь тут показує, що кандидат розрізняє «відхилення від специфікації» і «відхилення від контракту продукту».
Тест шле завідомо невалідний JSON, а сервер відповідає 500. Це очікувана поведінка?
Ні — це вже щонайменше «півбага» бекенда. Добре спроєктований сервер на будь-який кривий ввід має відповісти 400 чи 422, тобто свідомою помилкою клієнта, а не падати з необробленим винятком. 500 на сміттєвому вводі означає пропущену валідацію: якийсь шар коду спробував опрацювати те, що мав відхилити на вході. При цьому фіксувати варто обидві складові: тригер прийшов від клієнта чи тесту (кривий ввід), але реакція сервера — окремий дефект бекенда. Саме тому негативні тести з невалідними даними цінні: вони перевіряють не тільки повідомлення про помилку, а й те, що сервер відмовляє контрольовано, кодом 4xx, а не падає.
Автотест отримав 500. Як зрозуміти, чий це баг — фронта, бекенда чи самого тесту?
Базовий принцип: 5xx завжди повертає сервер, і це майже завжди вказує щонайменше на одну проблему на бекенді, але тригер міг прийти з будь-якої ланки, тому діагностика йде по кроках. Крок 1 — подивитися на сам запит: URL, метод, заголовки (особливо Authorization і Content-Type), тіло; якщо запит криво зібраний, імовірний тригер на боці фронтенду або тесту, але бекенд усе одно мав відповісти 4xx. Крок 2 — відтворити вручну той самий запит через curl чи Postman: відтворюється валідним запитом — кандидат на баг бекенда; не відтворюється, а тест падає — підозра зміщується на тест (не підставилась змінна, протух токен, крива фікстура, не те середовище). Крок 3 — логи сервера: 500 майже завжди лишає стектрейс, який прямо показує, де впало, — це і є доказ замість «здається». Крок 4 — відділити середовище від продукту: чи це справді 500, а не масові 502/503/504 через деплой чи навантаження; окремий патерн — 500 лише в CI під паралелізмом, а поодинці зелено, тоді розкопуй ізоляцію стану, гонки чи rate limit. Головна дисципліна: не заявляти причину без перевірки — «схоже на баг бекенда, стектрейс указує на конкретний контролер» коштує рівно стільки, скільки під ним доказів.
Три кейси, у яких статус-код — головний інструмент діагностики: перевірка самого факту редіректу з вимкненим автопрямуванням, тест ролей на 401 vs 403 із пасткою контракту 201/204, і покроковий тріаж «отримав 500 — чий це баг». Скрізь — що дивитися і чому.
Кейс 1. Перевіряєш редірект, а не фінальний 200
Задача — переконатися, що старий шлях назавжди переїхав на новий (http→https, /old→/new). Пастка в тому, що більшість клієнтів за замовчуванням самі йдуть за перенаправленням (follow redirects), тож у коді ти бачиш уже фінальний 200 — і проміжний 301 тест просто не помічає. Виняток — curl, який без -L за редіректом не піде:
curl -i https://app.example.com/old-path
HTTP/1.1 301 Moved Permanently
Location: https://app.example.com/new-path
Content-Length: 0
Щоб асертити сам редірект в автотесті, автопрямування треба вимкнути — тоді видно проміжний код і Location:
import { test, expect, request } from '@playwright/test';
test('старий шлях віддає постійний редірект на новий', async ({ playwright }) => {
const api = await request.newContext({ baseURL: 'https://app.example.com' });
const res = await api.get('/old-path', { maxRedirects: 0 });
expect(res.status()).toBe(301);
expect(res.headers()['location']).toBe('https://app.example.com/new-path');
});
Що дивитися і чому:
maxRedirects: 0— без нього тесту нема. З автопрямуваннямres.status()буде200кінцевої сторінки, і асерт на301неможливий у принципі. Вимкнув прямування — отримав саме проміжну відповідь.301vs302— це контракт, не косметика.301каже клієнтам і пошуковикам «кешуйте новий шлях назавжди». Якщо чекаєш301, а прилетів302(тимчасовий), — це легітимна претензія до контракту: тимчасовий редірект не дозволяє закешувати переїзд.- Заголовок
locationчитай у нижньому регістрі. У Playwrightres.headers()віддає імена заголовків уже нормалізованими в lower-case, тожheaders()['Location']повернеundefined. - Для
POST/PUT/DELETEдивись на307/308. Якщо редіректиться запит із тілом, а сервер віддає301/302, браузер історично може підмінити метод наGETі згубити тіло. Зберігають метод і тіло тільки307/308.
У UI-тесті надійніше не чекати на проміжний URL редіректу (він зʼявляється лише на мить — часте джерело флаку), а асертити фінальний стан сторінки.
Кейс 2. Тест ролей: 401 vs 403 і контракт 201/204
Класичний негативний сценарій: звичайний користувач не має лізти в адмін-ендпоінт. Інтуїція підказує асертити 401 — і це помилка. Юзер якраз автентифікований (токен валідний), тож правильна відмова — 403, а не 401.
import { test, expect, request } from '@playwright/test';
test('звичайний юзер не створює адмінів (403, не 401)', async ({ playwright }) => {
const api = await request.newContext({ baseURL: 'https://api.example.com' });
// логін звичайним юзером — токен валідний
const login = await api.post('/api/login', {
data: { email: 'user@example.com', password: 'secret' },
});
const { token } = await login.json();
const res = await api.post('/api/admins', {
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
data: { email: 'new-admin@example.com' },
});
// 401 тут — БРЕХНЯ: юзер автентифікований, йому просто не можна
expect(res.status()).toBe(403);
});
Що дивитися і чому:
401= «хто ти?»,403= «тобі можна?».401— не автентифікований (немає/протух/невалідний токен), лікується логіном.403— автентифікований, але роль без прав; повторний логін не допоможе. Асертиш401там, де сервер відповідає403, — тест зелений з неправильної причини або червоний на коректній поведінці.401↔403на реальному ендпоінті — це знахідка. Якщо продукт віддає401замість403(чи навпаки), тест на статус саме це й має зловити — це баг контролю доступу, а не причепка.- Звіряйся з контрактом: буває
404замість403. Частина API навмисно ховає закриті ресурси під404, щоб не розкривати сам факт їхнього існування. Тому асерт пиши під задокументовану поведінку конкретного API, а не «за інтуїцією».
Дзеркальна пастка — на позитивному боці. Ендпоінт створення після рефакторингу чесно віддає 201 Created замість 200, або DELETE віддає 204 No Content — і тест падає на рівному місці:
// створення повертає 201 + Location, а не 200
const created = await api.post('/api/users', {
headers: { 'Content-Type': 'application/json' },
data: { name: 'Grace', email: 'grace@example.com' },
});
expect(created.status()).toBe(201);
expect(created.headers()['location']).toBeTruthy();
// видалення повертає 204 БЕЗ тіла — .json() тут впаде
const removed = await api.delete('/api/users/43');
expect(removed.status()).toBe(204);
// await removed.json(); // ← помилка парсингу: тіла немає й не мало бути
201 замість 200 на створенні — часто легітимна вимога контракту (201 ще й несе Location з адресою нового ресурсу). А await res.json() після 204 дає помилку парсингу порожнього тіла: тест «червоний», хоча функціонал справний — просто тіла там і не буває.
Кейс 3. «Отримав 500 — це баг фронта, бекенда чи тесту?»
Це питання ставлять на кожній співбесіді. Плутанина в тому, що 5xx завжди повертає сервер, але спричинити його може будь-яка ланка. Порядок дій — від запиту до логів, без «здається».
Крок 1. Подивись на сам запит. URL, метод, Authorization, Content-Type, тіло. Якщо запит криво зібраний (немає обовʼязкового поля, зламаний JSON) — імовірний тригер на боці клієнта чи тесту. Але зафіксуй і бекенд-складову: на кривий ввід добре спроєктований сервер мусить відповісти 400/422, а не падати з 500. Неопрацьований 500 на смітті — це вже щонайменше півбага бекенда (пропущена валідація).
Крок 2. Відтвори вручну. Повтори точно той самий запит поза тестом:
curl -i -X POST https://staging.example.com/api/users \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"name":"Grace","email":"grace@example.com"}'
- Відтворюється валідним запитом → проблема стабільна, на боці сервера. Кандидат на баг бекенда.
- Не відтворюється, а тест усе одно падає → підозра зміщується на тест: не підставилась змінна, протух токен, крива фікстура, не те середовище.
Крок 3. Прочитай логи. 500 майже завжди лишає стектрейс (stack trace) у логах застосунку — він прямо показує рядок коду і причину падіння. Без вимірюваного доказу не заявляй root cause: стектрейс і є той доказ. Формулювання «схоже на баг бекенда, стектрейс указує на UsersController#create» коштує рівно стільки, скільки під ним доказів.
Крок 4. Відділи середовище від продукту. Це справді 500 чи насправді 502/503/504? Масові однотипні 5xx одразу після релізу — частіше про інфраструктуру (деплой у процесі, стейдж прогрівається, база під навантаженням), ніж про логіку продукту.
Швидкий висновок за симптомом:
| Що бачиш | Найімовірніший власник проблеми |
|---|---|
Запит валідний, 500 відтворюється вручну, у логах стектрейс | Баг бекенда |
Запит невалідний, але бекенд падає з 500 замість 4xx | Баг бекенда (пропущена валідація) + кривий ввід |
Запит невалідний, 500 не відтворюється чистим запитом | Баг тесту (шле не те) |
502/503/504, масові, привʼязані до деплою/навантаження | Середовище, не продукт |
500 лише в CI під паралелізмом, поодинці зелено | Ізоляція стану / гонки / rate limit (429) — розкопуй тест і дані |
Останній рядок — про підступний флак: паралельні воркери (workers) або незачищені дані дають 409 Conflict (email уже зайнятий) чи 429 Too Many Requests (спрацював rate limit). Симптом упізнаваний — локально зелено, у пайплайні плавучі падіння. Лікування: унікальні дані на прогін (email з таймстемпом/UUID), надійний teardown, повага до Retry-After у ретраях.
Анатомія та класи
- Знаю, що статус-код — трицифрове число у стартовому рядку відповіді, і що машина орієнтується на код, а не на текстову фразу (reason phrase), якої в HTTP/2 і HTTP/3 узагалі немає.
- Розумію, чому в тесті треба асертити
response.status() === 200, а не текстовий рядок. - Можу пояснити пʼять класів за першою цифрою (1xx, 2xx, 3xx, 4xx, 5xx) і що ключова лінія розлому — між 4xx («винен клієнт, виправляй запит») і 5xx («винен сервер»).
2xx — успіх
- Знаю різницю між
200 OK,201 Created(новий ресурс + заголовокLocation) і204 No Content(успіх без тіла). - Розумію, чому спроба зробити
res.json()після204падає з помилкою парсингу, хоча функціонал працює. - Можу пояснити, чому асерт
toBe(200)червоніє, коли ендпоінт створення чесно перейшов на201, і чому200замість201— це претензія до контракту.
3xx — перенаправлення і кеш
- Знаю різницю
301(постійно) vs302(тимчасово) і чому307/308зберігають метод і тіло, а301/302історично могли перетворитиPOSTнаGET. - Розумію правило: щоб перенаправити
POST/PUT/DELETEбез втрати тіла — це307/308, а не301/302. - Можу пояснити, чому клієнти автоматично йдуть за редіректами (крім
curlбез-L) і як явно перевірити редірект черезmaxRedirects: 0+ асертLocation. - Розумію, чому ланцюжок редіректів — часте джерело флаку в UI-тестах, і що чекати треба фінальний стан, а не проміжний URL.
- Можу пояснити
304 Not Modifiedяк серце умовних запитів:If-None-Matchпрацює зETag,If-Modified-Since— зLast-Modified, тіла немає. - Розумію, чому кеш і
304— перший підозрюваний, коли тест то зелений, то червоний залежно від порядку запуску.
4xx — помилка клієнта
- Знаю різницю
401(«хто ти?», не автентифікований, має бутиWWW-Authenticate) vs403(«тобі можна?», автентифікований, але без прав) і чому повторний логін при403не поможе. - Можу пояснити, чому в тесті ролей на закритий ендпоінт правильна відмова —
403, а асерт401бреше. - Розумію, чому деякі API навмисно віддають
404замість403, щоб не розкривати існування ресурсу. - Знаю, що
404маскує кілька причин (одрук у слагу, неправильний ID, фікстура не створила запис), тому перше питання — «а запис/маршрут узагалі є в цьому середовищі?». - Можу пояснити
409 Conflict(зайнятий email, дублікат, конкурентне редагування) як прямий сигнал проблеми ізоляції стану — лікування унікальними даними й teardown. - Знаю різницю
400(«не зрозумів синтаксис») vs422(«зрозумів, але вміст не проходить бізнес-валідацію») і що різні фреймворки віддають то одне, то інше — треба звірятися з контрактом. - Можу пояснити
429 Too Many Requests(rate limiting, заголовокRetry-After) як підступне джерело флаку під паралелізмом у CI.
5xx і діагностика
- Знаю різницю
500(виняток у застосунку),502(апстрім віддав некоректну відповідь),503(сервіс свідомо «зараз не можу») і504(шлюз не дочекався апстріму). - Розумію, чому масові
502/503/504після релізу — частіше про середовище, ніж про баг у продукті. - Можу пояснити діагностику
500по кроках: подивись запит → відтвори вручну (curl/Postman) → прочитай логи (стектрейс) → відділи середовище від продукту. - Розумію принцип, що навіть на криве сміття добрий бекенд має відповісти
4xx, а не впасти з500, і що причину не заявляють без вимірюваного доказу (стектрейс).
На що орієнтуватися в асерті автотесту — на код чи на текстову фразу статусу (reason phrase)?
Питання
У відповіді є код 200 і фраза OK. На що орієнтуватися в асерті й чому?