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

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

    Життєвий цикл запиту

    Зміст

    Між натисканням Enter в адресному рядку і появою сторінки на екрані відбувається довгий ланцюг подій. Для користувача це просто «сайт відкрився». Для AQA це послідовність окремих фаз, у кожної з яких власна тривалість, власні режими відмови й власні причини, чому автотест раптом «червоніє» без жодних змін у коді.

    Ця глава проходить повний шлях HTTPS-запиту фаза за фазою й після кожної показує, де тест може зависнути (hang) чи флакнути (flake). До кінця розділу стануть зрозумілими практичні питання на кшталт «чому запит працює в Postman, але падає в браузері» та «що означає pending навпроти запиту в DevTools» — і як повʼязати відповідь із поведінкою тест-фреймворку.

    Загальна карта шляху

    Спрощено повний шлях виглядає так:

    DNS  →  TCP  →  TLS  →  надсилання запиту  →  очікування (сервер)  →  відповідь  →  рендер
    

    Кожна стрілка — це час і можлива точка збою. Важлива інтуїція для тестувальника: більшість «повільних» або «флакі» тестів ламаються не на етапі рендеру, який ми бачимо, а на невидимих ранніх фазах — резолвінгу імені, встановленні зʼєднання чи очікуванні першого байта від сервера. Тому спершу пройдемо шлях по порядку, а потім зведемо симптоми в одну таблицю.

    Крок 1. DNS: від імені до адреси

    Браузер не вміє підключатися до example.com — йому потрібна IP-адреса. Перетворення імені на адресу робить DNS (Domain Name System).

    Пошук іде каскадом кешів, від найближчого до найдальшого:

    1. Кеш самого браузера.
    2. Кеш операційної системи (і файл hosts, який має пріоритет над мережевим запитом, якщо там є запис).
    3. Резолвер провайдера або корпоративний DNS.
    4. Якщо ніде немає — рекурсивний обхід: кореневі сервери → сервери зони (TLD, напр. .com) → авторитетний сервер домену.

    є

    немає

    є

    немає

    є

    немає

    Імʼя: example.com

    Кеш браузера?

    IP-адреса

    Кеш ОС / hosts?

    Резолвер провайдера?

    Рекурсивний обхід: корінь → TLD → авторитетний

    є

    немає

    є

    немає

    є

    немає

    Імʼя: example.com

    Кеш браузера?

    IP-адреса

    Кеш ОС / hosts?

    Резолвер провайдера?

    Рекурсивний обхід: корінь → TLD → авторитетний

    Кожен запис має час життя (TTL, Time To Live), після якого кеш вважається застарілим і резолвінг повторюється. Перевірити резолвінг вручну можна так:

    # показати, у яку адресу резолвиться імʼя, і хто відповів
    dig +short staging.example.com
    nslookup staging.example.com

    Де флакне тест. DNS — класичне джерело «магічних» падінь, особливо на стейджингах:

    • Запис у /etc/hosts перекриває реальний DNS. Тест на машині розробника ходить на локальний бекенд, а в CI — на справжній, і поведінка різна.
    • Свіжостворений домен ще не розійшовся по кешах (пропагація), тому частина агентів бачить його, частина — ні.
    • Повільний або перевантажений корпоративний резолвер додає сотні мілісекунд до кожного запиту, і тест, що ходить на десятки ендпоінтів, стабільно впирається в таймаут.

    Практичний висновок: якщо тест падає лише в одному оточенні, перша гіпотеза — не код, а те, куди насправді резолвиться імʼя.

    Крок 2. TCP: встановлення зʼєднання

    Маючи IP, браузер відкриває TCP-зʼєднання. Це «потискання руки» з трьох кроків (three-way handshake):

    клієнт → сервер:  SYN
    сервер → клієнт:  SYN-ACK
    клієнт → сервер:  ACK
    
    СерверКлієнтСерверКлієнтЗʼєднання відкритеSYNSYN-ACKACKСерверКлієнтСерверКлієнтЗʼєднання відкритеSYNSYN-ACKACK

    Після цього канал відкритий і по ньому можна передавати дані. Зʼєднання не одноразове: HTTP тримає його відкритим (keep-alive) і повторно використовує для наступних запитів, щоб не платити щоразу за handshake (а на HTTPS — ще й за TLS). Саме тому перший запит до нового хоста завжди «дорожчий» за наступні.

    Де флакне тест. На рівні TCP збої зазвичай грубі й помітні:

    • ECONNREFUSED — на порту ніхто не слухає. Сервіс іще не піднявся (типова гонка в CI: тест стартував раніше, ніж застосунок), або неправильний порт.
    • ETIMEDOUT на зʼєднанні — пакети йдуть «у нікуди»: фаєрвол, неправильна мережа, VPN.
    • Вичерпання пулу зʼєднань під навантаженням — нові запити стають у чергу (див. розділ про pending).

    Крок 3. TLS: захищене рукостискання

    Літера «S» у HTTPS — це TLS (Transport Layer Security). Поверх уже відкритого TCP-зʼєднання клієнт і сервер домовляються про шифрування. Оглядово фаза така:

    • Клієнт надсилає ClientHello: які версії TLS і набори шифрів (cipher suites) він підтримує. Тут же передається імʼя хоста (SNI, Server Name Indication) — щоб сервер, який обслуговує багато доменів на одній IP, знав, який саме сертифікат віддати.
    • Сервер відповідає ServerHello, надсилає свій сертифікат (ланцюг до довіреного центру сертифікації) і параметри.
    • Сторони узгоджують спільний ключ і переходять на шифрований канал.
    СерверКлієнтСерверКлієнтПерехід на шифрований каналClientHello (версії, шифри, SNI)ServerHello + сертифікатУзгодження спільного ключаСерверКлієнтСерверКлієнтПерехід на шифрований каналClientHello (версії, шифри, SNI)ServerHello + сертифікатУзгодження спільного ключа

    Ключова для розуміння таймінгів деталь — кількість «кругів» (round trips, RTT):

    ВерсіяРаунди на рукостисканняПримітка
    TLS 1.2~2 RTTбільша затримка
    TLS 1.3~1 RTTі 0-RTT при відновленні сесії

    Тобто TLS — це реальний, вимірюваний внесок у затримку до першого байта, особливо на «холодному» зʼєднанні, де до нього додається ще й RTT самого TCP-рукостискання. Режим 0-RTT (нульовий раунд для даних) можливий лише при відновленні раніше встановленої сесії й має відомий компроміс — ризик повторного відтворення (replay) раннього трафіку, тому сервери вмикають його вибірково.

    Де флакне тест. Найпоширеніша причина падінь тут — сертифікати на нестабільних оточеннях:

    • самопідписаний (self-signed) або прострочений сертифікат на стейджингу;
    • невідповідність імені: запит іде на staging.example.com, а сертифікат виданий на example.com (той самий SNI, що передається в ClientHello, і має збігтися з іменем у сертифікаті).

    Браузер за замовчуванням відхиляє такі зʼєднання. У тест-фреймворках для цього є явний вимикач перевірки — але користуйся ним свідомо, лише для тестових оточень:

    // Playwright: ігнорувати помилки HTTPS (тільки для тестових доменів!)
    const context = await browser.newContext({ ignoreHTTPSErrors: true });

    Крок 4. Надсилання запиту

    Канал захищений — браузер надсилає власне HTTP-запит: рядок запиту (метод, шлях, версія), заголовки (headers) і, за потреби, тіло (body).

    GET /api/users/42 HTTP/1.1
    Host: api.example.com
    Accept: application/json
    Cookie: session=abc123
    Origin: https://app.example.com
    User-Agent: Mozilla/5.0 ...

    Тут зʼявляються заголовки, які додає саме браузер і які згодом пояснять різницю з Postman: Origin, Cookie, User-Agent, Referer. Розробник у коді їх не пише — браузер підставляє автоматично, виходячи з контексту сторінки й свого сховища cookie.

    Крок 5. Відповідь і TTFB

    Далі — очікування. Сервер отримує запит, обробляє його (ходить у базу, рахує, серіалізує) і починає віддавати відповідь. Момент, коли до клієнта доходить перший байт відповіді, задає ключову метрику — TTFB (Time To First Byte).

    Важливо: TTFB — це не час обробки на сервері окремо. У нього входить усе, що передувало появі першого байта:

    Складова TTFBЩо це
    DNSрезолвінг імені
    TCPвстановлення зʼєднання
    TLSзахищене рукостискання
    Requestчас на передачу запиту до сервера
    Server processingвласне робота бекенду
    Waitingчас, поки перший байт іде назад

    Якщо на шляху був редирект, його час теж входить у TTFB: стандартна веб-метрика веде відлік від початку навігації, тож кожен проміжний «стрибок» додається до підсумку.

    Нюанс. Різні інструменти рахують TTFB від різних точок відліку: від старту навігації разом із редиректами або від моменту відправлення конкретного запиту. Перш ніж порівнювати числа з DevTools і стороннього моніторингу, звір, від чого саме кожен веде відлік.

    Практична цінність TTFB для AQA: якщо він великий, а решта (завантаження тіла, рендер) нормальна, проблема майже напевно на бекенді або в мережі до нього, а не у фронтенді. Це вже конкретний напрям для баг-репорту, а не розмите «сторінка гальмує».

    У DevTools час запиту розкладено на фази (Queued, Stalled, DNS Lookup, Initial connection, SSL, Request sent, Waiting for server response / TTFB, Content Download). Уміння читати цю розкладку — базова навичка: вона одразу каже, на якій саме фазі втрачено час.

    Крок 6. Рендер

    Отримавши HTML, браузер парсить його й будує DOM (Document Object Model) — дерево обʼєктів сторінки. Паралельно з CSS будується CSSOM, а з їх поєднання постає дерево рендеру, яке браузер розкладає (layout) і малює (paint). Цей конвеєр і називають критичним шляхом рендеру (critical rendering path).

    Натрапляючи на посилання на підресурси (CSS, JS, шрифти, зображення), браузер ініціює нові запити — і для кожного цикл DNS→TCP→TLS→відповідь повторюється (часто вже поверх перевикористаного зʼєднання). Частина ресурсів блокує рендер: зовнішній CSS і синхронний <script> без async/defer зупиняють парсинг, поки не завантажаться й не виконаються. JavaScript, своєю чергою, може змінювати DOM і породжувати ще запити (XHR/fetch) — уже після початкового завантаження сторінки.

    Тут важливі дві події, на які часто «чекають» тести:

    • DOMContentLoaded — HTML розпарсено й DOM побудовано (підресурси можуть іще вантажитися).
    • load — завантажено все, зокрема картинки й стилі.

    Де флакне тест. Це найтонше місце для автоматизації. Тест, який чекає лише на load, може «прокинутися» до того, як дані підвантажилися через fetch і відмалювалися — бо load засвідчує лише завантаження початкових ресурсів сторінки, а не подальших XHR/fetch, які JavaScript часто запускає вже після цієї події (або відповідь на які приходить пізніше за неї). Звідси головне правило сучасних фреймворків: чекай на елемент або на конкретний мережевий запит, а не на подію завантаження сторінки чи фіксовану паузу. Жорсткі sleep/waitForTimeout — головне джерело флаку: на швидкій машині це зайве очікування, на повільній — недостатнє.

    // погано: гонка з реальним часом завантаження
    await page.waitForTimeout(3000);
    
    // добре: чекаємо на факт, а не на секунди
    await page.getByRole('row', { name: 'John Doe' }).waitFor();
    
    // або чекаємо саме на потрібну відповідь API
    await page.waitForResponse(r => r.url().includes('/api/users') && r.ok());

    Ланцюг редиректів (redirect chain)

    Не завжди перша відповідь фінальна. Сервер може відповісти статусом 3xx і заголовком Location, і браузер піде за новою адресою. Таких «стрибків» може бути кілька поспіль — це і є ланцюг редиректів.

    КодНазваМетод при переходіПостійний?
    301Moved Permanentlyісторично може змінити на GETтак
    302Foundісторично може змінити на GETні
    303See Otherзавжди на GETні
    307Temporary Redirectзберігає метод і тілоні
    308Permanent Redirectзберігає метод і тілотак

    Типові ланцюги: http://https://, потім example.comwww.example.com, потім //login. Кожен крок — це окремий повний запит із власними DNS/TCP/TLS, тобто редиректи додають затримку.

    Де флакне тест. Часті пастки:

    • Тест перевіряє URL одразу після переходу й ловить проміжний, а не кінцевий, бо ланцюг ще не завершився.
    • API-тест, який має бачити тіло відповіді, налаштований не йти за редиректами (302 без автопереходу) — і отримує порожнє тіло замість очікуваних даних.
    • Циклічний редирект (напр. неправильно налаштована авторизація) — браузер обриває його з помилкою після певної кількості кроків.

    Подивитися весь ланцюг зручно так:

    # показати кожен крок редиректу і фінальний код
    curl -sIL https://example.com | grep -i -E 'HTTP/|location'

    Коли виникає preflight

    CORS (Cross-Origin Resource Sharing) — механізм браузера, що контролює запити з однієї origin на іншу (origin = схема + домен + порт; зміна будь-якої з трьох частин робить запит крос-доменним). Для частини «небезпечних» крос-доменних запитів браузер спершу надсилає попередній запит методом OPTIONS — це і є preflight. Він питає сервер: «чи можна мені зробити те, що я збираюся?». Лише після дозвільної відповіді летить справжній запит.

    Спрощено, preflight не потрібен для «простих» запитів і потрібен для решти:

    УмоваПростий запит (без preflight)Небезпечний (з preflight)
    МетодGET, HEAD, POSTPUT, PATCH, DELETE тощо
    Content-Typeобмежений набір (form-urlencoded, multipart, text/plain)application/json
    Заголовкилише зі стандартного «безпечного» спискубудь-які кастомні (напр. X-Request-Id)

    До «безпечного» (safelisted) списку заголовків специфікація Fetch відносить лише кілька — насамперед Accept, Accept-Language, Content-Language й Content-Type (останній лише з тими самими трьома значеннями), ще й з обмеженнями на довжину та символи значення. Усе поза цим списком вважається кастомним і тригерить preflight. Тому типовий REST-виклик із Content-Type: application/json майже завжди спричиняє preflight. Виглядає він так:

    OPTIONS /api/users/42 HTTP/1.1
    Origin: https://app.example.com
    Access-Control-Request-Method: DELETE
    Access-Control-Request-Headers: authorization, content-type
    HTTP/1.1 204 No Content
    Access-Control-Allow-Origin: https://app.example.com
    Access-Control-Allow-Methods: GET, POST, DELETE
    Access-Control-Allow-Headers: authorization, content-type
    Access-Control-Max-Age: 600
    СерверБраузерСерверБраузерДозвіл отриманоOPTIONS (preflight)204, Access-Control-Allow-*DELETE /api/users/42200 OKСерверБраузерСерверБраузерДозвіл отриманоOPTIONS (preflight)204, Access-Control-Allow-*DELETE /api/users/42200 OK

    Access-Control-Max-Age дозволяє браузеру кешувати дозвіл і не повторювати OPTIONS перед кожним викликом.

    Окремий нюанс — запити з обліковими даними (cookie або Authorization). Щоб браузер прийняв відповідь на такий крос-доменний запит, сервер має повернути Access-Control-Allow-Credentials: true, і при цьому Access-Control-Allow-Origin не може бути * — потрібен конкретний origin. Це часта причина, чому запит із cookie ламається саме в браузері.

    Де флакне тест. По-перше, preflight подвоює кількість запитів — у DevTools ти побачиш OPTIONS перед кожним «справжнім» викликом, і це нормально. По-друге, класична пастка: сам запит правильний, але сервер неправильно відповідає на OPTIONS — і браузер блокує основний запит ще до відправлення. У тесті це виглядає як «дані не приходять», хоча сервер до основного запиту навіть не дійшов. Дивись на OPTIONS, а не лише на цільовий запит.

    Що означає стан pending

    Pending навпроти запиту в DevTools означає, що браузер уже ініціював запит, але ще не отримав (повної) відповіді. Це не помилка сама по собі — це «в процесі». Але якщо запит висить у pending довго, причини бувають різні:

    • Waiting (TTFB): запит пішов, сервер думає й мовчить — довга обробка на бекенді.
    • Queued / Stalled: запит навіть не відправлено. Браузер обмежує кількість одночасних зʼєднань до однієї origin (у багатьох браузерах — близько 6 на хост для HTTP/1.1); зайві запити чекають своєї черги. Під час навантаження чи великої кількості паралельних викликів це помітно. На HTTP/2 запити мультиплексуються в одному зʼєднанні, і ця межа фактично зникає (натомість діє серверний ліміт одночасних потоків).
    • Blocked: запит стримується (напр. очікує звільнення зʼєднання або на нього впливає інша логіка браузера).
    • Довгі зʼєднання: для стрімів, SSE чи WebSocket-апгрейдів pending — нормальний і очікуваний стан, бо зʼєднання тримається відкритим навмисно.

    Де флакне тест. Якщо тест «зависає» на дії, яка тригерить мережевий виклик, а в DevTools цей виклик у вічному pending — фреймворк чесно чекає на відповідь, якої немає, аж до свого таймауту. Діагностика: подивитися, у якій саме фазі застряг запит (черга проти очікування сервера), — це одразу розводить «повільний бекенд» і «браузер уперся в ліміт зʼєднань».

    Чому запит успішний у Postman, але не в браузері

    Дуже частий репорт: «в Postman усе працює, а в застосунку/тесті — ні». Причина майже завжди в тому, що Postman і браузер — різні клієнти з різними правилами. Postman просто надсилає HTTP-запит. Браузер додає цілий шар політик безпеки й контексту.

    АспектБраузерPostman
    Same-origin / CORSзастосовує, може заблокувати крос-доменний запитігнорує повністю
    Preflight (OPTIONS)шле автоматично за потребине шле
    Cookies / сесіяпідставляє автоматично для доменулише якщо задав вручну
    Заголовки Origin, Refererставить самне ставить, поки не додаси
    Змішаний контент (mixed content)блокує HTTP-ресурси на HTTPS-сторінцібайдуже
    CSP, Service Worker, кеш браузеравпливають на запитвідсутні

    Практичні наслідки для діагностики:

    • CORS. Найтиповіший випадок: у Postman 200 OK, у браузері — помилка CORS у консолі. Це не «баг фронтенду», а те, що сервер не дозволяє крос-доменний доступ, і лише браузер це перевіряє.
    • Авторизація через cookie. У браузері ти залогінений і cookie летить сам; у Postman запит іде без сесії — і або падає, або (гірше) працює під іншим користувачем, бо ти вручну підставив інший токен.
    • Заголовки. Сервер може вимагати Origin чи певний Content-Type, які браузер ставить, а ти в Postman — ні (або навпаки).

    Висновок для тестувальника: «в Postman працює» — це доказ того, що бекенд віддає дані, а не того, що з браузера теж спрацює. Це різні рівні перевірки, і плутати їх у баг-репорті не можна.

    Timeout: що це і які бувають

    Таймаут (timeout) — це верхня межа часу, який компонент готовий чекати, перш ніж визнати операцію невдалою. Таймаутів на шляху запиту багато, і вони діють на різних рівнях. Плутанина між ними — часте джерело незрозумілих падінь.

    РівеньТаймаутЩо обмежує
    Мережа/ОСconnection timeoutчас на встановлення TCP/TLS-зʼєднання
    HTTP-клієнтread / response timeoutчас очікування відповіді після відправлення запиту
    HTTP-клієнтidle timeoutмаксимум простою в зʼєднанні
    Сервер/проксіgateway timeoutбекенд не встиг відповісти проксі → клієнт бачить 504
    Тест-фреймворкnavigation timeoutзавантаження сторінки
    Тест-фреймворкaction / command timeoutокрема дія (клік, введення)
    Тест-фреймворкassertion / expect timeoutскільки перечікувати умову перевірки
    Тест-фреймворкtest timeoutувесь тест цілком

    Орієнтовні дефолти, щоб було з чим порівнювати (звіряй з версією свого інструмента):

    // Playwright (playwright.config)
    // тест: 30 000 мс; expect: 5 000 мс;
    // action/navigation за замовчуванням не мають окремого ліміту
    // й обмежені лише таймаутом тесту
    export default {
      timeout: 30_000,
      expect: { timeout: 5_000 },
    };

    Дефолти на сьогодні (вони змінюються між релізами — звіряй з докою своєї версії): Playwright — таймаут тесту 30 с, expect 5 с, а actionTimeout і navigationTimeout за замовчуванням не обмежені, їх стримує загальний таймаут тесту; Cypress — defaultCommandTimeout 4 с, pageLoadTimeout 60 с, requestTimeout 5 с, responseTimeout 30 с.

    Ключова ідея: коли тест падає з таймаута, спершу зрозумій, чий саме таймаут спрацював. 504 від шлюзу, обрив на рівні HTTP-клієнта й «expect не дочекався елемента» — три різні діагнози з різними виправленнями. І окреме правило: не «лікуй» флак простим підняттям таймауту. Більший таймаут ховає проблему, робить тести повільнішими й перетворює швидкі падіння на довгі зависання.

    Retry: повторні спроби

    Retry — повторення операції після невдачі. Як і таймаути, ретраї живуть на кількох рівнях:

    • Мережа. TCP сам ретрансмітить втрачені пакети — це нижче за HTTP і ти цього не бачиш.
    • HTTP-клієнт / інфраструктура. Клієнти й проксі можуть повторювати запити (часто на 502/503/504 чи мережеві обриви). Розумний ретрай робить паузу, що зростає між спробами (експоненційний відступ, exponential backoff), щоб не добивати вже перевантажений сервіс.
    • Тест-фреймворк. Механізм ретраю впалих тестів (напр. retries у конфізі) — щоб приховати рідкісний флак.

    Головне застереження — ідемпотентність (idempotency). Ідемпотентний запит можна безпечно повторити, бо повтор не змінює результат (умовно — GET, а також DELETE конкретного ресурсу: видалити вже видалене дає той самий стан). А от повтор POST, що створює замовлення, може створити дублікат. Тому «просто додати retry» — не безпечне рішення за замовчуванням.

    Для AQA ретрай тестів — двосічна річ. З одного боку, він гасить рідкісний зовнішній флак і не валить весь прогон. З іншого — маскує реальні нестабільності: тест, що проходить із другої спроби, це не «зелений» тест, а прихований баг у тесті або в продукті. Розумний підхід: ретраї ввімкнені, але кожен «passed on retry» видно у звіті й хтось за ним іде розбиратися, а не радіє зеленому статусу.

    // Playwright: 2 ретраї лише в CI; локально бачимо флак одразу
    export default {
      retries: process.env.CI ? 2 : 0,
    };

    Зведення: де на шляху ламається тест

    ФазаСимптом у тестіТипова причинаКуди дивитися
    DNSпадає лише в одному оточенніhosts, повільний резолвер, пропагаціяdig, /etc/hosts
    TCPECONNREFUSED, ETIMEDOUTсервіс не піднявся, порт/фаєрволлоги запуску, порт
    TLSпомилка сертифікатаself-signed / прострочений на стейджингусертифікат, ignoreHTTPSErrors
    Запит/відповідьтаймаут очікуванняповільний бекенд, великий TTFBфаза Waiting у DevTools
    Редиректне той URL / порожнє тілоловимо проміжний крок, не йдемо за 3xxcurl -IL, Location
    CORS/preflight«дані не приходять»сервер зле відповідає на OPTIONSзапит OPTIONS, консоль
    Рендерелемента ще немаєчекаємо на load, а не на фактwaitFor на елемент/відповідь
    pending назавждитест зависає до таймаутучерга зʼєднань або мовчазний серверфаза запиту в DevTools

    Наскрізний принцип: тест бачить лише кінцевий результат — «зелено» чи «червоно», — але причина майже завжди на конкретній фазі шляху. Тому за будь-якого нестабільного падіння спершу локалізуй фазу (мережа проти сервера проти рендеру), а вже потім став гіпотезу. І скрізь, де можеш, замінюй очікування часу на очікування факту — саме це відрізняє стабільний автотест від такого, що «іноді проходить».