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

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

    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 — успіх

    Успіх теж буває різним, і плутати його різновиди — типова помилка в тестах.

    КодНазваКолиЧи є тіло відповіді
    200OKЗагальний успіх (частіше GET)Так, з даними
    201CreatedСтворено новий ресурс (частіше POST)Зазвичай так + заголовок Location
    204No 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.

    СерверКлієнтСерверКлієнтfollow redirects увімкненоGET /old-path301 Moved Permanently, Location: /new-pathGET /new-path200 OKСерверКлієнтСерверКлієнтfollow redirects увімкненоGET /old-path301 Moved Permanently, Location: /new-pathGET /new-path200 OK

    301 vs 302 і оглядово 307/308

    Тут головне питання — постійність і збереження методу.

    КодНазваПостійністьЧи зберігає метод/тіло
    301Moved PermanentlyПостійноІсторично метод міг змінитися (POST→GET)
    302FoundТимчасовоІсторично метод міг змінитися (POST→GET)
    307Temporary RedirectТимчасовоМетод і тіло зберігаються
    308Permanent 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. Це і зручно, і небезпечно:

    • Якщо треба перевірити сам факт редіректу (наприклад, httphttps або старий 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.

    СерверКлієнтСерверКлієнтзберігає копію + валідаторбере актуальну копію з кешуGET /api/config200 OK, ETag: "a1b2c3"GET /api/config, If-None-Match: "a1b2c3"304 Not Modified (без тіла)СерверКлієнтСерверКлієнтзберігає копію + валідаторбере актуальну копію з кешуGET /api/config200 OK, ETag: "a1b2c3"GET /api/config, If-None-Match: "a1b2c3"304 Not 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 Unauthorized403 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: запит був нормальний, а зламався сам сервер. Клієнт нічого не зробив не так (принаймні формально). Розрізняти підкласи важливо, бо вони вказують на різні місця поломки в інфраструктурі.

    КодНазваДе ламаєтьсяТипова причина
    500Internal Server ErrorУ самому застосункуНеопрацьований виняток (exception), падіння коду
    502Bad GatewayНа шлюзі/проксіАпстрім (upstream) віддав некоректну/поламану відповідь
    503Service UnavailableУ застосункуСервіс тимчасово не тягне: перевантаження, деплой, обслуговування
    504Gateway TimeoutНа шлюзі/проксіАпстрім не відповів вчасно

    Щоб відчути різницю між 502/503/504, уяви типову топологію: браузер → зворотний проксі/балансувальник (reverse proxy, наприклад nginx) → застосунок → база даних.

    Браузер

    Проксі / балансувальник

    Застосунок

    База даних

    502 / 504: проблема на шлюзі

    500 / 503: проблема в застосунку

    Браузер

    Проксі / балансувальник

    Застосунок

    База даних

    502 / 504: проблема на шлюзі

    500 / 503: проблема в застосунку

    • 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 — частіше про інфраструктуру, ніж про логіку.

    Ні

    Так

    Ні

    Так

    Так

    Ні

    Так

    Ні

    Отримав 5xx

    Запит валідний?

    Кривий ввід клієнта/тесту + бекенд мав дати 4xx

    Відтворюється вручну?

    Баг тесту: шле не те

    У логах стектрейс?

    Баг бекенда

    Масові 502/503/504?

    Середовище / інфраструктура

    Ні

    Так

    Ні

    Так

    Так

    Ні

    Так

    Ні

    Отримав 5xx

    Запит валідний?

    Кривий ввід клієнта/тесту + бекенд мав дати 4xx

    Відтворюється вручну?

    Баг тесту: шле не те

    У логах стектрейс?

    Баг бекенда

    Масові 502/503/504?

    Середовище / інфраструктура

    Зведена таблиця для швидкого висновку:

    Що бачишНайімовірніший власник проблеми
    Запит валідний, 500 відтворюється вручну, у логах стектрейсБаг бекенда
    Запит невалідний, але бекенд падає з 500 (а не 4xx)Баг бекенда (пропущена валідація) + кривий ввід від клієнта/тесту
    Запит невалідний, 500 не відтворюється чистим запитомБаг тесту (шле не те)
    502/503/504, часто масові, привʼязані до деплою/навантаженняСередовище/інфраструктура, не продукт
    500 лише в CI під паралелізмом, поодинці зеленоІзоляція стану / гонки (races) / rate limit — розкопуй тест і дані

    Головна дисципліна: не заявляй причину, доки не перевірив. Спершу подивись запит, відтвори, прочитай логи — і лише тоді став діагноз. Формулювання «схоже на баг бекенда, стектрейс указує на UsersController#create» коштує рівно стільки, скільки під ним доказів; «напевно фронт винен» без відтворення не коштує нічого.