Життєвий цикл запиту
Зміст
Між натисканням Enter і появою сторінки відбувається довгий ланцюг подій. Для користувача це просто «сайт відкрився». Для AQA це послідовність фаз, у кожної з яких власна тривалість, власні режими й власні причини, чому тест раптом червоніє без жодної зміни в коді.
Ця глава — повний виклад шляху запиту на сайті: решта глав розділу посилаються сюди по карту фаз, а вона відсилає до них по деталі кожної. Мета одна — вміти за хвилину сказати, яка саме фаза зʼїла час або зламала перевірку, до того як хтось почав правити код тесту.
Сім фаз одного запиту
Життєвий цикл запиту (request lifecycle) — послідовність фаз між натисканням Enter і появою сторінки: DNS → TCP → TLS → надсилання запиту → очікування сервера → відповідь → рендер. Кожна стрілка тут — це час і можлива точка збою.
Розкладка не абстрактна — саме нею інструменти й міряють час: панель Network розводить кожен запит на ті самі фази окремими смужками (до її таблиці ми повернемося наприкінці). Дрібниця, яка щоразу спантеличує: узгодження шифрування довідник кладе всередину фази Initial connection — разом із TCP- і його можливими повторами.
Наша практика (не канон). Далі — спостереження команд, під яке окремого джерела немає: більшість «повільних» і ламаються не на видимому етапі рендеру, а на невидимих ранніх фазах — резолвінгу імені, встановленні зʼєднання чи очікуванні першого байта. Тому діагностику варто починати з початку шляху, а не з кінця.
Резолвінг імені: фаза, якої не видно
Браузер не вміє підключатися до example.com — йому потрібна адреса. Перекладає імʼя в адресу DNS (Domain Name System): розподілена ієрархічна система з простору імен, серверів імен і резолверів. Резолвер операційної системи (stub-резолвер) ієрархію сам не обходить — він перекладає роботу на рекурсивний сервер імен, а той іде звичним ланцюгом: кореневі сервери → сервери зони верхнього рівня → авторитетний сервер домену, який і повертає A-запис. Кожен запис живе стільки, скільки дозволяє TTL, і орієнтир самого RFC для типового вузла — порядок днів.
Звідси симптоми. Час «холодної» резолюції додається до часу відкриття сторінки — іноді саме він перетворює нормальний прогін на перевищення . Якщо середовище нещодавно змінило IP, а десь у ланцюжку живий старий запис, тести стукають за застарілою адресою, і чекати доводиться саме TTL. Окремо стоїть локальна таблиця хостів (файл hosts): це резерв або доповнення до DNS, і що з них має пріоритет, залежить від конфігурації вузла, тож запис на машині розробника чи на CI- тихо спрямовує тести на інший стенд. Деталі протоколу й типи записів — глава «DNS, IP, порти та мережа».
Наша практика (не канон). Джерела такого правила не дають, але воно економить години: якщо тест падає лише в одному оточенні, перша гіпотеза — не код, а те, куди насправді резолвиться імʼя (запис у hosts, повільний корпоративний резолвер, непропагований домен). Так само з практики, а не зі специфікації, взято звичний опис пошуку як каскаду «кеш браузера → кеш ОС і hosts → резолвер провайдера».
Рукостискання: скільки їх на шляху HTTPS-запиту
Рукостискання (handshake) — узгодження параметрів зʼєднання перед тим, як піде корисний трафік. На шляху звичайного HTTPS-запиту їх щонайменше два: транспортне TCP і криптографічне TLS. Для реалтайму додається третє — відкривальне рукостискання WebSocket, зроблене як HTTP-запит Upgrade.
TCP. Зʼєднання встановлюється тристороннім рукостисканням SYN → SYN-ACK → ACK. Кроків саме три, бо підтвердження й власний SYN сервера обʼєднані в одне повідомлення; після цього зʼєднання — логічний канал, який ідентифікує пара сокетів.
TLS. Протокол складається з двох компонентів: рукостискання, що автентифікує сторони, узгоджує криптографічні параметри й встановлює спільний ключовий матеріал, і record-протоколу, який цими параметрами захищає трафік. Клієнт шле ClientHello, сервер відповідає ServerHello — і в TLS 1.3 усі повідомлення рукостискання після ServerHello уже зашифровані, зокрема сертифікат. У ClientHello їде імʼя хоста (SNI, Server Name Indication), щоб сервер із багатьма доменами на одній IP віддав потрібний сертифікат; це імʼя має збігтися з іменем у сертифікаті.
Це вимірюваний внесок у : специфікація сама називає звичайне рукостискання TLS 1.3 «1-RTT handshake». Режим 0- економить іще один обхід, коли сторони мають спільний ключ PSK — отриманий зовні або попереднім рукостисканням, а не лише відновленням сесії; плата названа прямо: властивості безпеки раннього трафіку слабші, тобто він уразливий до повторного відтворення (replay). Добра новина — платиться це раз на зʼєднання: HTTP тримає канал відкритим (keep-alive) і перевикористовує його, тому перший запит до нового хоста дорожчий за наступні.
WebSocket. Зʼєднання відкривається звичайним GET із заголовками Upgrade: websocket, Connection: Upgrade і Sec-WebSocket-Key, а успіх означає рівно 101 Switching Protocols із Sec-WebSocket-Accept. Будь-який інший статус означає, що рукостискання не завершилося і далі діє звичайна семантика HTTP: 403 тут не «зламаний веб-сокет», а незавершений апгрейд. Далі — глави «WebSockets і реалтайм» і «HTTPS, TLS і безпека».
Наша практика (не канон). Дві речі тут ідуть від досвіду, а не від специфікацій. Поширену оцінку «TLS 1.2 коштує близько 2 RTT» жодне джерело реєстру не підтверджує — це галузева цифра. І найчастіша причина падінь саме на фазі TLS — сертифікати нестабільних оточень (самопідписаний або прострочений на , невідповідність імені), а вимикач перевірки на кшталт ignoreHTTPSErrors вмикають свідомо й лише для тестових доменів.
Запит пішов: TTFB як межа між «сервер думає» і «дані йдуть»
Запит складається з методу, шляху, заголовків і тіла. Частину заголовків код задати не може: Fetch перелічує їх поіменно як forbidden request-header — серед них Cookie, Origin, Referer, Host, Content-Length. Їх ставить сам браузер із контексту сторінки й свого сховища, і саме звідси беруться типові репорти «у Postman працює, а в браузері ні». Виняток, який плутають найчастіше: User-Agent у переліку немає — браузер підставляє його лише за відсутності власного значення, тому підміна в тесті й працює.
Заголовки — не єдина відмінність браузера від консольного клієнта. Браузер здатен не відправити запит узагалі: (mixed content) на HTTPS-сторінці він або підвищує до HTTPS, або блокує, а CSP (Content Security Policy) обмежує, звідки сторінці дозволено вантажити ресурси. Обидва механізми спрацьовують ще до мережевої фази, і в панелі такий запит зазвичай або відсутній, або одразу червоний.
Далі — очікування. Момент, коли до клієнта доходить перший байт відповіді, задає (time to first byte). І тут головна пастка: під однією абревіатурою живуть два різні вимірювані обʼєкти.
| Що міряють | Від чого рахують | Що входить |
|---|---|---|
| Навігаційний TTFB (MDN, web.dev) | старт навігації | резолвінг DNS, TCP- і TLS-рукостискання, час редиректів, передача запиту |
Фаза Waiting (TTFB) у DevTools | момент, коли запит уже відправлено | один обхід затримки плюс час, який сервер витратив на підготовку відповіді |
MDN формулює перший дослівно: цей час «includes DNS lookup and establishing the connection using a TCP handshake and TLS handshake if the request is made over HTTPS». Довідник DevTools описує другий: «This timing includes 1 round trip of latency and the time the server took to prepare the response», причому DNS Lookup, Initial connection і Request sent стоять фазами перед ним.
Практична цінність пряма: якщо велике саме Waiting (TTFB), а Content Download і рендер нормальні, проблема на бекенді або в мережі до нього — це вже напрям для баг-репорту, а не розмите «сторінка гальмує». Структуру запиту — глава «HTTP: методи, структура, заголовки».
Наша практика (не канон). Сам поділ TTFB на два обʼєкти — наше співставлення джерел: кожне означує свій предмет, але прямо про два предмети не каже жодне. Наслідок робочий: числа з різних інструментів «у лоб» не порівнюються, і в баг-репорті варто писати, звідки взято цифру.
Кеш: фаза, яка вміє обірвати шлях на самому початку
Не кожен запит доходить до сервера. (HTTP caching) — зберігання вже отриманих відповідей у проміжному сховищі, щоб не тягнути їх мережею повторно: поки копія свіжа, вона віддається без жодного мережевого походу. Ще ближче до сторінки стоїть Service Worker — фоновий скрипт, який працює як між вебзастосунком, браузером і мережею й перехоплює навігаційні та ресурсні запити. Довідник DevTools фіксує це в описі фази Content Download: браузер отримує відповідь «either directly from the network or from a service worker». Тобто рядок у панелі є, а мережевого обміну могло не бути взагалі.
Два схожі на вигляд стани варто розрізняти. Свіжа копія з кешу означає, що сервера навіть не питали, і зміна на бекенді до тесту не доїде, доки не мине свіжість. 304 Not Modified — навпаки, живий запит: сервер підтвердив збережену відповідь і не передав тіла, тобто ревалідація працює. Правила свіжості й механіку 304 розбирає глава «Кешування».
Редирект і preflight: фази, що подовжують ланцюг
Кеш шлях вкорочує, а дві інші фази — подовжують.
Редирект (redirect) — відповідь із кодом 3xx і заголовком Location, куди клієнту треба піти далі. Браузери й більшість HTTP-клієнтів ідуть за перенаправленням автоматично; помітний виняток — curl, який без прапорця -L за редиректом не піде. Ціну MDN називає прямо: кожен редирект коштує зайвого HTTP-запиту, тобто має відчутний вплив на продуктивність — і саме тому час редиректів входить у навігаційний TTFB. Що при цьому стається з методом і тілом:
| Код | Метод і тіло при переході |
|---|---|
301, 302 | незмінність гарантована лише для GET; інші методи клієнт може підмінити на GET і втратити тіло |
303 | навмисно переводить на GET, тіло втрачається |
307, 308 | «Method and body not changed» — і метод, і тіло зберігаються |
Звідси правило: перенаправляти POST/PUT/DELETE без втрати тіла — це 307/308, а не 301/302. Петля редиректів зазвичай є проблемою конфігурації сервера, і якщо сервер її помічає, то віддає 500. Увесь ланцюг видно однією командою:
curl -sIL https://example.com/ | grep -i -E 'HTTP/|location'
Класи кодів — глава «HTTP статус-коди».
Preflight — додатковий раунд перед «справжнім» запитом. Для запитів, складніших за ті, що дозволяє HTML-форма, браузер спершу шле окремий OPTIONS із заголовками Access-Control-Request-Method і Access-Control-Request-Headers, і лише після дозвільної відповіді летить основний виклик. Тригери: метод поза GET/HEAD/POST; Content-Type поза трьома значеннями (application/x-www-form-urlencoded, multipart/form-data, text/plain) — тому типовий REST-виклик із application/json preflight спричиняє; будь-який заголовок поза безпечним переліком. Успішним preflight вважається лише за статусу в діапазоні 200–299, а Access-Control-Max-Age дозволяє кешувати дозвіл.
Чотири деталі просто для тестів. Preflight ніколи не надсилається з обліковими даними, тож вішати перевірку авторизації на маршрут OPTIONS не можна — вона його завалить. Запит з обліковими даними (cookie або HTTP-автентифікація) вимагає від сервера Access-Control-Allow-Credentials: true, і * у Access-Control-Allow-Origin при цьому неприпустимий — потрібне конкретне . Заголовок Authorization — окремий випадок: Fetch відносить його до CORS non- request-header, тож Access-Control-Allow-Headers: * його не покриває — сервер має назвати цей заголовок поіменно. І один логічний виклик дає два записи в мережі: якщо очікування привʼязане не до того запису, тест плаватиме. Механізм цілком — глава «CORS і політика одного походження».
Скільки зʼєднань і якою версією протоколу
Досі ми йшли одним запитом, а реальна сторінка тягне десятки підресурсів — і тут вмикається паралелізм. Зʼєднання HTTP/1.x серіалізує запити: наступний іде після відповіді на попередній. Щоб це обійти, браузер відкриває до одного домену кілька зʼєднань — колишній дефолт 2–3, нинішній поширений — шість. Довідник DevTools фіксує число прямо в поясненні черги («There are already six TCP connections open for this origin») і додає межу: правило стосується лише HTTP/1.0 і HTTP/1.1. На HTTP/2 картина протилежна — специфікація каже, що клієнт не повинен відкривати більш ніж одне зʼєднання на пару «хост+порт». Тому гіпотеза «сторінка гальмує, бо забагато дрібних файлів» на HTTP/1.1 правдоподібна, а на HTTP/2 — ні. Саме число шість при цьому — спостережувана поведінка браузерів, а не норма протоколу: у RFC його немає.
Версію теж не «вмикають» — про неї домовляються на старті зʼєднання (узгодження версії протоколу, protocol negotiation). Рекомендований шлях — узгодити протокол усередині TLS-рукостискання через ALPN, без додаткових обходів; браузери піднімають HTTP/2 лише поверх TLS. Наслідок: локальний стенд на http:// у браузері завжди піде по HTTP/1.1, і це не дефект. Мультиплексування, й HTTP/3 — глава «HTTP/2 і HTTP/3: еволюція протоколу».
Рендер: «відповідь прийшла» ще не означає «видно»
Отримавши HTML, браузер будує DOM, паралельно з CSS — CSSOM, з їх поєднання постає дерево рендерингу, яке розкладається (layout) і малюється (paint), а шари складаються в підсумкову картинку (composite). Уся послідовність від отримання HTML до перших пікселів називається критичним шляхом рендерингу (critical rendering path).
Ключове для нашої теми: рендер породжує нові запити. Натрапляючи на підресурси — CSS, скрипти, шрифти, зображення, — браузер ініціює власні запити, і для кожного цикл повторюється, часто вже поверх перевикористаного зʼєднання; частина цих запитів блокує подальший розбір документа.
Дві події, на які часто «чекають» тести: DOMContentLoaded — HTML розпарсено й DOM побудовано, підресурси можуть іще вантажитися; load — завантажено всю сторінку разом із залежними ресурсами (стилі, скрипти, iframe, картинки), окрім лінивих. Але головне правило сучасних фреймворків інше: чекати на факт, а не на секунди — фіксована пауза на швидкій машині зайва, а на повільній недостатня.
// погано: гонка з реальним часом завантаження
await page.waitForTimeout(3000);
// добре: чекаємо на факт
await page.getByRole('row', { name: 'Some user name' }).waitFor();
// або саме на потрібну відповідь API
await page.waitForResponse((r) => r.url().includes('/api/users') && r.ok());
Етапи конвеєра розбирає глава «Архітектура браузера й рендеринг», а те, коли між ними виконується JavaScript, — глава «Виконання JavaScript та event loop».
Наша практика (не канон). Два уточнення досвіду, яких джерела прямо не роблять. Перше: жодна з двох подій не засвідчує подальших XHR/fetch — JavaScript часто запускає їх уже після load, тож тест, що чекає лише на подію завантаження, може прокинутися раніше за дані. Друге: серед блокувальних запитів окремо називають зовнішній CSS і синхронний script без async/defer; джерело говорить лише про «some requests are blocking», без переліку.
Що з цього видно у вкладці Network
Панель записує всі запити автоматично — але лише поки DevTools відкриті, тож перше правило діагностики: відкрити її до відтворення проблеми. Колонка Time показує повну тривалість від старту запиту до останнього байта, — запити один відносно одного (типово за часом старту), а розкладка Timing розводить смужку по фазах:
| Фаза | Що означає |
|---|---|
Queueing / Stalled | запит іще не відправлено: він стоїть у черзі — наприклад, бо на це походження вже відкрито шість зʼєднань |
DNS Lookup | браузер резолвить IP-адресу запиту |
Initial connection | встановлення зʼєднання: TCP-рукостискання (з можливими повторами) і узгодження SSL |
Request sent | передача запиту до сервера |
Waiting (TTFB) | один обхід затримки плюс час підготовки відповіді на сервері |
Content Download | отримання тіла — з мережі або від service worker |
Звідси й прочитання стану pending: браузер запит уже ініціював, але (повної) відповіді ще не отримав — сам собою це «в процесі», а не помилка. Дві колонки, без яких половина цієї глави лишається невидимою, у дефолтний набір не входять: Protocol (h2 для HTTP/2, h3 для HTTP/3) і Connection ID (у якому зʼєднанні пішов запит) — їх вмикають правою кнопкою по шапці таблиці. І дрібниця для баг-репортів: експорт HAR типово «санітизований», із нього прибрано Cookie, Set-Cookie й Authorization. Панель як інструмент — глава «DevTools: вкладка Network і дебаг».
Наша практика (не канон). У довіднику фаз не описані ні стан Blocked, ні навмисно довгі зʼєднання, тож наступне — з практики: для стрімів, SSE й WebSocket-апгрейдів вічний pending нормальний, бо канал тримають відкритим свідомо, і плутати його із зависанням не варто.
Діагностика фазами: від симптому до фази
Тест бачить лише кінцевий результат — зелено або червоно, тоді як причина сидить на конкретній фазі. Тож перше питання одне: яка фаза зʼїла час або зламалася. Найшвидший спосіб розвести резолвінг, зʼєднання й сервер — розкласти час по шарах. curl дає готові змінні, і кожна відлічується від старту, тож вартість фази — це різниця сусідніх:
curl -sS -o /dev/null -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer}\n' https://example.com/
Великий time_namelookup — гальмує резолвінг; резолвінг швидкий, а time_connect довгий — проблема зʼєднання; зʼєднання й TLS швидкі, а time_starttransfer великий — думає сервер. Якщо ж усі чотири числа малі, а сторінка «гальмує», справа вже на клієнті — у рендері або в JavaScript.
Друга вісь — чий таймаут спрацював. Вони живуть на різних рівнях: --connect-timeout у клієнта обмежує лише фазу встановлення зʼєднання; read/response обмежує очікування відповіді, а idle — простій у вже відкритому зʼєднанні; , який не дочекався бекенда, віддає клієнту 504; а таймаути тест-фреймворку (навігація, дія, перевірка, тест) — це вже про очікування в самому тесті, і їхню ієрархію розбирає розділ про інструменти автоматизації.
Третя вісь — , теж багаторівневі. TCP сам повторно надсилає втрачені сегменти нижче за HTTP, і цього не видно; HTTP-клієнти й проксі повторюють запити, часто на 502/503/504, а ретрай зазвичай робить паузу, що зростає між спробами (), щоб не добивати перевантажений сервіс. Чи безпечно повторювати, вирішує : GET, HEAD, OPTIONS, TRACE, PUT і DELETE ідемпотентні — стан сервера після одного й після N викликів однаковий; POST, PATCH і CONNECT — ні, тому автоповтор POST, що створює замовлення, здатен породити дубль.
| Симптом | Найімовірніша фаза | Куди дивитися |
|---|---|---|
| падає лише в одному оточенні | резолвінг імені | time_namelookup, запис у hosts, TTL |
| помилка сертифіката | TLS-рукостискання | імʼя в сертифікаті проти SNI, термін дії |
| довге очікування без даних | сервер | фаза Waiting (TTFB) у Timing |
| запит стоїть, хоч сервер вільний | черга зʼєднань | Queueing/Stalled, колонка Connection ID |
| не той URL або порожнє тіло | ланцюг редиректів | коди 3xx і Location, прапорець -L |
| «дані не приходять» у браузері, а в curl усе гаразд | preflight | запис OPTIONS перед основним запитом |
| елемента ще немає, хоч відповідь прийшла | рендер | очікування факту замість паузи |
Наша практика (не канон). Канонічного джерела під це немає, але правило варте того, щоб його записали: піднімати таймаут замість діагнозу — означає ховати проблему. Більший таймаут не робить тест стабільним, він робить його повільним і перетворює швидке падіння на довге зависання.
Типові помилки
- Виглядає як «повільний фронтенд», а насправді велика фаза
Waiting (TTFB): думає сервер, і жодна оптимізація верстки цього не змінить. - Виглядає як «сервер не отримав запит», а насправді відповідь прийшла з кешу або від service worker — рядок у панелі є, мережевого обміну не було.
- Виглядає як «дані не приходять», а насправді не пройшов preflight: браузер заблокував основний запит ще до відправлення, і бекенд його не бачив.
- Виглядає як «редирект безкоштовний», а насправді кожен крок ланцюга — окремий HTTP-запит із ціною, яку MDN називає відчутною.
- Виглядає як «браузер завжди тримає шість зʼєднань», а насправді це поточна практика для HTTP/1.0 і HTTP/1.1; на HTTP/2 клієнт не повинен відкривати більш ніж одне.
- Виглядає як «спрацював
load, отже дані на місці», а насправді подія засвідчує лише ресурси самої сторінки, а не дані, які JavaScript довантажує пізніше. - Виглядає як «у Postman
200, отже баг фронтенду», а насправді CORS перевіряє лише браузер: клієнти поза ним заголовкиAccess-Control-*ігнорують.
Підсумок
- Шлях запиту — послідовність фаз, і кожна стрілка між ними коштує часу й уміє зламатися окремо від інших.
- Рукостискань на звичайному HTTPS-запиті щонайменше два (TCP і TLS), плюс третє для WebSocket; платяться вони раз на зʼєднання, тому перший запит до хоста дорожчий за наступні.
- TTFB — межа між «сервер думає» і «дані йдуть», але це два різні виміри: навігаційний включає резолвінг і рукостискання, фаза в DevTools — ні.
- Кеш уміє обірвати шлях на початку, а редирект і preflight — подовжити його зайвими повними запитами.
- Діагностика починається з локалізації фази — розкладкою часу або фазами Timing, — і лише потім переходить до гіпотези про причину.
Можливі питання
- «Що відбувається між натисканням Enter і появою сторінки?» Питання на структуру мислення. Сильна відповідь називає фази по порядку й одразу додає, що кожна — це і час, і окремий режим відмови.
- «Скільки рукостискань на шляху HTTPS-запиту?» Перевіряють, чи не злипаються у вас TCP і TLS в одне. Очікують «щонайменше два» плюс третє для WebSocket, зроблене як HTTP-запит
Upgrade. - «Що таке TTFB і що саме він міряє?» Дивляться на точність: чи згадаєте, що під однією абревіатурою живуть два різні виміри, і чи скажете, звідки взято число.
- «Тест падає лише на одному стенді. Ваші дії?» Міні-кейс, у якому слухають порядок: спершу фаза, потім гіпотеза, і лише тоді код.
- «Чому запит працює в curl, але падає в браузері?» Питання про межу відповідальності клієнта. Очікують: браузер додає шар, якого поза ним немає, — CORS і preflight, автоматичні
CookieйOrigin, кеш і service worker. - «Чому не можна просто підняти таймаут?» Перевіряють інженерну зрілість: сильна відповідь спершу питає, чий таймаут спрацював.
Джерела
- Chrome DevTools — Network features reference — фази запиту в розкладці Timing: резолвінг, встановлення зʼєднання з TCP-рукостисканням і узгодженням SSL, очікування першого байта, завантаження тіла.
- MDN — Critical rendering path — що відбувається з відповіддю після отримання і як із неї постає сторінка.
Резолвінг імені: фаза, якої не видно
- RFC 1034 — Domain Names: Concepts and Facilities — складники DNS, роль резолвера, кеш і TTL разом з орієнтиром «порядок днів» для типового вузла.
- RFC 1123 — Requirements for Internet Hosts -- Application and Support — stub-резолвер покладається на рекурсивний сервер; таблиця хостів як резерв або доповнення з конфігурованим пріоритетом.
- curl — manual page (--write-out variables) — час резолюції як окрема, вимірювана частина загального часу запиту.
Рукостискання: скільки їх на шляху HTTPS-запиту
- RFC 9293 — Transmission Control Protocol (TCP) — тристороннє рукостискання, чому кроків саме три, зʼєднання як пара сокетів.
- RFC 8446 — TLS 1.3 — два компоненти протоколу, шифрування всього після
ServerHello, «1-RTT handshake» і 0-RTT із зовнішнім або успадкованим PSK разом з його ціною. - RFC 6066 — TLS Extensions: Extension Definitions — SNI у
ClientHelloяк спосіб отримати правильний сертифікат від сервера з багатьма доменами. - RFC 6455 — The WebSocket Protocol — відкривальне рукостискання як HTTP-запит
Upgradeі101як єдина ознака успіху. - MDN — Connection management in HTTP/1.x — відкритого зʼєднання, через яке перший запит дорожчий за наступні.
Запит пішов: TTFB як межа між «сервер думає» і «дані йдуть»
- WHATWG Fetch Standard — поіменний перелік forbidden request-header і правило, за яким
User-Agentдодається лише за відсутності власного значення. - MDN — HTTP messages — склад запиту: метод, шлях, заголовки, тіло.
- MDN — Mixed content — незахищені підресурси на захищеній сторінці браузер або підвищує до HTTPS, або блокує.
- W3C — Content Security Policy Level 3 — політика керує тим, які ресурси сторінці дозволено завантажувати й виконувати.
- MDN Glossary — Time to first byte (TTFB) — навігаційний TTFB включає резолвінг DNS, TCP- і TLS-рукостискання.
- web.dev — Time to First Byte (TTFB) — розклад навігаційного TTFB на суму фаз, зокрема час редиректів.
- Chrome DevTools — Network features reference —
Waiting (TTFB)як окрема фаза після встановлення зʼєднання: один RTT плюс підготовка відповіді.
Кеш: фаза, яка вміє обірвати шлях на самому початку
- RFC 9111 — HTTP Caching — кеш як сховище відповідей, перевикористання свіжої копії замість походу мережею, семантика
304 Not Modified. - MDN — HTTP caching — правила кешування диктує сервер заголовками відповіді.
- MDN — Service Worker API — як проксі між застосунком, браузером і мережею, що перехоплює навігаційні та ресурсні запити.
- Chrome DevTools — Network features reference — фаза
Content Downloadяк отримання відповіді з мережі або від service worker.
Редирект і preflight: фази, що подовжують ланцюг
- MDN — Redirections in HTTP —
3xxплюсLocation, поведінка методу й тіла для301/302/303/307/308, ціна редиректу в продуктивності, петля й відповідь500. - RFC 9110 — HTTP Semantics — семантика класу
3xx: запит нормальний, але для завершення потрібна ще одна дія. - curl — manual page (--write-out variables) —
curlповторює запит за новою адресою лише з-L/--location. - web.dev — Time to First Byte (TTFB) — час редиректів як складова навігаційного TTFB.
- MDN — Cross-Origin Resource Sharing (CORS) — preflight як окремий
OPTIONS-запит, його заголовки,Access-Control-Max-Ageі два записи на один логічний виклик. - WHATWG Fetch Standard — умови, за яких preflight потрібен, безпечний перелік заголовків і значень
Content-Type, «ok»-статус як умова успіху, заборона надсилати preflight з обліковими даними.
Скільки зʼєднань і якою версією протоколу
- MDN — Connection management in HTTP/1.x — серіалізація запитів на зʼєднанні, кілька паралельних зʼєднань на домен і число шість як поточна практика, а не норма.
- RFC 9113 — HTTP/2 — вимога не відкривати більш ніж одне зʼєднання на пару «хост+порт».
- Chrome DevTools — Network features reference — шість одночасних TCP-зʼєднань на походження як причина черги і межа правила для HTTP/1.0 і HTTP/1.1.
- High Performance Browser Networking — HTTP/2 — три способи узгодити версію і рекомендація робити це через TLS з ALPN.
- caniuse — HTTP/2 protocol (features-json) — у браузерах HTTP/2 підтримується лише поверх TLS, а сервер має вміти ALPN.
Рендер: «відповідь прийшла» ще не означає «видно»
- MDN — Critical rendering path — DOM і CSSOM, дерево рендерингу, layout і paint, підресурси з власним циклом запиту, блокувальні запити.
- web.dev — Rendering performance (pixel pipeline) — пʼять етапів конвеєра, зокрема composite як завершальний.
- MDN — Document: DOMContentLoaded event — момент, коли HTML розпарсено й DOM побудовано.
- MDN — Window: load event — момент, коли завантажено сторінку разом із усіма залежними ресурсами, крім лінивих.
- Playwright — Auto-waiting (actionability) — очікування придатності елемента до дії замість паузи.
- Playwright — Network — очікування конкретної мережевої відповіді.
- Selenium — Waiting Strategies — стратегії очікування замість фіксованого сну.
Що з цього видно у вкладці Network
- Chrome DevTools — Network features reference — автоматичний запис лише за відкритих DevTools, колонка
Time, Waterfall за часом старту, повна розкладка фаз Timing, колонкиProtocolіConnection IDпоза дефолтним набором, санітизований експорт HAR.
Діагностика фазами: від симптому до фази
- curl — manual page (--write-out variables) — змінні
time_namelookup,time_connect,time_appconnectіtime_starttransfer, кожна від старту запиту, і--connect-timeoutяк обмеження лише фази зʼєднання. - RFC 9110 — HTTP Semantics — ідемпотентність як рівність ефекту одного й кількох однакових запитів;
504як відповідь шлюзу, що не дочекався. - MDN — HTTP request methods — перелік .
- MDN — Idempotent (Glossary) —
POST,PATCHіCONNECTяк неідемпотентні. - RFC 9293 — Transmission Control Protocol (TCP) — повторна передача втрачених сегментів як механізм самого транспорту.
- Playwright — Timeouts — таймаути тест-фреймворку як окремий рівень поверх мережевих.
- Chrome DevTools — Network features reference — фази Timing як інструмент локалізації втраченого часу.
Що відбувається між натисканням Enter і появою сторінки?
Спершу браузер має дізнатися, куди стукати: імені example.com для підключення замало, тож іде резолвінг у IP-адресу. Далі відкривається транспортний канал (TCP), поверх нього сторони домовляються про шифрування (TLS), і лише після цього летить сам HTTP-запит. Потім — пауза очікування, у якій сервер готує відповідь, прихід першого байта, завантаження тіла й побудова сторінки з отриманого HTML. Сильна відповідь не зупиняється на переліку: варто одразу сказати, що кожен перехід між фазами коштує часу й ламається окремо від сусідів, тому «сайт не відкрився» — це сім різних діагнозів, а не один. І остання деталь, яку часто забувають: рендер не завершує ланцюг, а запускає нові його копії — під кожен CSS, скрипт, шрифт і картинку браузер шле власний запит.
Скільки рукостискань відбувається на шляху одного HTTPS-запиту?
Щонайменше два: транспортне TCP і криптографічне TLS. TCP домовляється трьома кроками — SYN, SYN-ACK, ACK; кроків саме три, бо сервер склеює підтвердження чужого SYN і власний SYN в одне повідомлення. TLS поверх нього автентифікує сторони, узгоджує шифри й виводить спільний ключовий матеріал, після чого record-протокол уже захищає трафік цими параметрами. Якщо сторінка відкриває реалтайм-канал, зʼявляється третє — WebSocket, і воно оформлене як звичайний HTTP-запит з Upgrade. Практичний висновок: усе це платиться один раз на зʼєднання, а не на кожен запит, бо HTTP тримає канал відкритим і перевикористовує його — тому перше звернення до нового хоста завжди дорожче за наступні, і саме воно найчастіше не влазить у жорсткий .
Навіщо в ClientHello їде SNI і як це проявляється в тестах?
SNI (Server Name Indication) — розширення TLS, яким клієнт називає імʼя хоста ще на початку рукостискання. Без нього сервер, що тримає десяток доменів на одній IP-адресі, не знає, чий сертифікат віддавати: HTTP-заголовок Host він побачить пізніше, вже всередині шифрованого каналу. Умова успіху проста — імʼя з SNI має відповідати імені в сертифікаті, який приїде у відповідь. Звідси найтиповіша поломка нестабільних оточень: стенд підняли за новим доменом, сертифікат лишився старий, і тест валиться на TLS ще до того, як застосунок побачив хоч один запит. Помилка при цьому виглядає як проблема застосунку, хоча жодного HTTP тут ще не було.
Що змінилося в TLS 1.3 щодо видимості рукостискання і що дає режим 0-RTT?
У TLS 1.3 усе, що йде після ServerHello, уже зашифроване — включно з сертифікатом сервера. Для QA це означає, що в перехопленому трафіку деталей рукостискання не роздивитися так, як раніше, і діагностувати сертифікат доводиться інструментами, а не оком по дампу. Звичайне рукостискання специфікація описує як таке, що коштує одного обходу туди-назад. Режим 0- дозволяє клієнту надіслати дані ще в першому повідомленні, якщо в сторін уже є спільний попередній ключ — отриманий або з попереднього рукостискання, або взагалі поза протоколом. Економія реальна, але плата названа прямо: у ранніх даних слабші гарантії безпеки, зокрема їх можна відтворити повторно. Тому 0-RTT — не той , який вмикають «щоб швидше», і для неідемпотентних операцій він небезпечний.
Які заголовки запиту тест не може підставити з коду і чому?
Fetch тримає поіменний перелік заборонених заголовків запиту — серед них Cookie, Origin, Referer, Host і Content-Length. Їх ставить сам браузер: з контексту сторінки й власного сховища, і код сторінки чи тесту переписати їх не може. Логіка тут не в примсі, а в безпеці: якби скрипт довільно малював Origin чи Cookie, уся модель й cookie-політик перестала б щось означати. Практичний наслідок — половина репортів «у Postman працює, а в браузері ні»: клієнт ставить що завгодно, браузер — ні. Виняток, на якому ловлять найчастіше: User-Agent у цьому переліку відсутній, браузер лише підставляє своє значення, коли власного немає, — тому підміна User-Agent у тесті цілком робоча, а підміна Cookie тим самим способом мовчки ігнорується, і тест зеленіє на непідміненому запиті.
Чому запит працює в curl, але падає в браузері?
Тому що браузер додає цілий шар, якого поза ним не існує. По-перше, він сам ставить Cookie й Origin і не дозволяє їх переписати, тож автентифікація в браузері й у консолі може працювати по-різному. По-друге, вмикається CORS: виклик, складніший за форму, спершу породжує окремий preflight, і без дозвільної відповіді основний запит не піде. По-третє, браузер здатен не відправити запит узагалі: незахищений підресурс на захищеній сторінці він або переводить на HTTPS, або відрізає, а CSP обмежує, звідки сторінці дозволено вантажити ресурси. По-четверте, відповідь могла приїхати не з мережі, а з кешу чи від service worker. Консольні клієнти заголовки Access-Control-* просто ігнорують, тому 200 у curl нічого не доводить про поведінку в браузері.
Що таке TTFB і чому два інструменти показують під ним різні числа?
— момент, коли до клієнта доходить перший байт відповіді, тобто межа між «сервер думає» і «дані йдуть». Проблема в тому, що абревіатура покриває два різні предмети вимірювання. Навігаційний TTFB, про який пишуть MDN і web.dev, рахується від старту навігації й тягне в себе резолвінг імені, обидва рукостискання, час усіх редиректів і передачу самого запиту. Фаза Waiting (TTFB) у DevTools рахується від моменту, коли запит уже пішов, і містить лише один обхід плюс час підготовки відповіді — резолвінг і встановлення зʼєднання стоять у розкладці окремими смужками перед нею. Тому числа з різних інструментів не порівнюються в лоб, і в баг-репорті обовʼязково пишуть, звідки взято цифру. Діагностична цінність від цього тільки виграє: якщо роздута саме фаза очікування, а завантаження тіла й рендер у нормі, шукати треба на бекенді або в мережі до нього, а не у верстці.
У панелі мережі рядок запиту є, а бекенд його не бачив. Як таке можливо?
Найімовірніше, відповідь прийшла не з мережі. Поки збережена копія свіжа, кеш віддає її без жодного походу до сервера — і зміна, яку щойно викотили на бекенд, до тесту просто не доїде, доки свіжість не мине. Ще ближче до сторінки стоїть service worker: це фоновий скрипт- між застосунком, браузером і мережею, який перехоплює навігаційні та ресурсні запити й може відповісти самотужки. Довідник DevTools це прямо фіксує — тіло відповіді браузер отримує або з мережі, або від , і в обох випадках у панелі буде рядок. Розрізняти треба два схожі стани: свіжа копія з кешу означає, що сервера не питали взагалі, а 304 Not Modified — навпаки, живий запит, у якому сервер підтвердив копію й зекономив лише тіло. Перше — потенційна причина «бачу стару версію», друге — ознака справної ревалідації.
Що станеться з POST, якщо сервер відповість 301, і чому редирект не безкоштовний?
Метод може перетворитися на GET, а тіло — зникнути: для 301 і 302 незмінність гарантована лише для GET, решту методів клієнт має право підмінити. Виглядає це підступно — фінальний статус 200, тест зелений, а дані на сервер не доїхали, бо друга половина ланцюга була вже іншим запитом без тіла. 303 робить те саме навмисно й за визначенням переводить на GET. Зберігають і метод, і тіло лише 307 і 308 — тому перенаправляти POST, PUT чи DELETE треба саме ними. Щодо ціни: кожен крок ланцюга — це окремий повний HTTP-запит, і MDN називає вплив на продуктивність відчутним; час редиректів недарма входить у навігаційний TTFB. Дві деталі для перевірки: curl без -L за перенаправленням не піде, а петля редиректів — це зазвичай конфігурація сервера, і помітивши її, сервер віддає 500.
Коли браузер шле preflight і чому на маршрут OPTIONS не можна вішати авторизацію?
Preflight зʼявляється тоді, коли крос-оріджин запит складніший за той, що вміє звичайна HTML-форма. Тригерів три: метод поза GET, HEAD і POST; Content-Type поза трьома дозволеними значеннями — саме через це звичайний REST-виклик із JSON додатковий раунд і породжує; нарешті, будь-який заголовок, якого немає в безпечному переліку. Браузер спершу шле окремий OPTIONS із Access-Control-Request-Method і Access-Control-Request-Headers, і лише отримавши дозвіл у діапазоні 200–299, відправляє основний виклик. Тепер головне: preflight ніколи не несе облікових даних — ні cookie, ні Authorization. Тому перевірка авторизації, повішена на OPTIONS, гарантовано завалить його і заблокує весь для браузерних клієнтів, хоча в curl усе працюватиме бездоганно. Окремо памʼятають і про облікові дані в основному запиті: сервер має віддати Access-Control-Allow-Credentials: true, а зірочка в Access-Control-Allow-Origin при цьому не годиться — потрібне конкретне походження.
Чому один виклик API дає два рядки в панелі мережі і як це ламає очікування в тесті?
Бо preflight — це окремий мережевий запит, а не службова частина основного. У панелі зʼявляються два записи: спершу OPTIONS, потім реальний POST чи PATCH. Очікування на відповідь, написане надто широко — «дочекайся першої відповіді з цього URL», — має шанс зачепитися саме за OPTIONS, а він приходить раніше й нічого не каже про дані. Тест після цього йде далі, коли справжня відповідь ще в дорозі, і починає плавати без видимої причини. Лікується це точністю предиката: звіряти не лише URL, а й метод і статус відповіді. Побічний бонус — Access-Control-Max-Age дозволяє кешувати дозвіл, тож у частині прогонів другого рядка не буде взагалі, і очікування, привʼязане до наявності OPTIONS, теж стане нестабільним.
Скільки паралельних зʼєднань браузер відкриває до одного домену?
Залежить від версії протоколу, і це головне в питанні. Одне зʼєднання HTTP/1.x пропускає запити строго по черзі — доки не прийшла відповідь, наступний не рушить. Щоб обійти обмеження, браузер відкриває до одного походження кілька зʼєднань — колись їх було два-три, зараз поширене число шість, і саме на нього посилається DevTools, пояснюючи, чому запит стоїть у черзі. Важливо, що шістка — спостережувана поведінка браузерів, а не норма протоколу: у RFC її немає. Для HTTP/2 правило протилежне й нормативне: більш ніж одне зʼєднання на ту саму пару хоста й порту клієнту відкривати не належить. Практичний наслідок для аналізу продуктивності: гіпотеза «сторінка гальмує, бо тягне сотню дрібних файлів» на HTTP/1.1 має сенс, а на HTTP/2 — ні, і перевіряється вона колонкою Protocol, яку в панелі треба вмикати руками.
Чому локальний стенд на http:// завжди йде по HTTP/1.1, хоча сервер уміє HTTP/2?
Бо версію протоколу не «вмикають» галочкою — про неї домовляються на старті зʼєднання. Рекомендований і фактично основний шлях у вебі — узгодження всередині TLS-рукостискання через ALPN, тобто без жодного зайвого обходу мережею. Браузери піднімають HTTP/2 виключно поверх TLS, тому на чистому http:// домовлятися просто нема де, і зʼєднання лишається на HTTP/1.1. Для тестувальника звідси два висновки. Перший: побачити h2 на локальному стенді без сертифіката — неможливо, і це не дефект конфігурації. Другий: заміри продуктивності, зняті локально по HTTP/1.1, не переносяться на прод, який працює по HTTP/2, — там інша модель паралелізму, і вузькі місця будуть інші.
Чим DOMContentLoaded відрізняється від load і чому чекати на них у тесті недостатньо?
DOMContentLoaded спрацьовує, коли HTML розібрано й DOM побудовано, — підресурси при цьому можуть іще вантажитися. load настає пізніше: сторінку завантажено разом із залежними ресурсами — стилями, скриптами, картинками, iframe, — крім лінивих. Різниця в часі буває суттєвою, але для автотестів важливіша спільна межа обох подій: жодна з них не засвідчує даних, які JavaScript тягне вже після завантаження сторінки. Тест, що чекає на load і одразу таблицю, прокидається раніше за її вміст. Тому сучасні фреймворки радять чекати на факт, а не на подію чи секунди: на появу конкретного елемента, на конкретну мережеву відповідь, на стан застосунку. Фіксована пауза — найгірший з варіантів: на швидкій машині вона зайва, на повільній недостатня, а падіння від неї виглядає як баг продукту.
У панелі запит висить у стані pending. Це помилка?
Сам собою — ні. pending означає рівно те, що браузер запит ініціював, а повної відповіді ще не отримав; це стан «у процесі», а не діагноз. Далі важливий контекст. Для звичайного REST-виклику довгий pending — привід дивитися, на якій фазі стоїть час: якщо велика смужка Queueing чи Stalled, запит навіть не відправлено і він чекає вільного зʼєднання; якщо Waiting (TTFB) — думає сервер. А для стрімів, SSE й піднятого WebSocket вічний pending абсолютно нормальний: канал тримають відкритим свідомо, дані течуть по ньому, і плутати це із зависанням не варто. Помилково заведений баг «запит ніколи не завершується» на WebSocket-зʼєднанні — класика.
Тест падає лише на одному стенді, код у нього однаковий. Ваші дії?
Тут перевіряють не знання, а порядок дій: спершу фаза, потім гіпотеза, і лише тоді код. Перша підозра при «падає тільки тут» — не логіка тесту, а те, куди насправді резолвиться імʼя: запис у локальному файлі hosts на CI- або машині розробника тихо відправляє трафік на інший стенд, а недавня зміна IP у середовищі може ще не розійтися, бо старий запис живе стільки, скільки дозволяє TTL, — а типовий орієнтир тут порядок днів, не хвилин. Друга підозра — сертифікат: на нестабільних оточеннях він буває самопідписаний, прострочений або виданий на інше імʼя, і тоді падіння станеться на TLS, ще до першого HTTP-запиту. Далі перевіряють різницю в конфігурації: , , CORS-налаштування, версія застосунку. Найдешевший спосіб розвести ці версії — розкласти час запиту по фазах з консолі й подивитися, яка з них зʼїла час або обірвалася.
Як розкласти час запиту по фазах, маючи лише консоль?
curl вміє віддати таймінги окремими змінними у --write-out: час резолвінгу, час встановлення TCP-зʼєднання, час завершення TLS і час до першого байта. Ключова тонкість — усі вони відлічуються від старту запиту, а не від кінця попередньої фази, тож вартість кожної фази це різниця сусідніх чисел, і рахувати її треба самому.
curl -sS -o /dev/null \
-w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer}\n' \
https://example.com/
Читається це просто. Великий перший показник — винен резолвінг. Резолвінг швидкий, а зʼєднання довге — проблема на транспорті або в мережі до хоста. Зʼєднання й TLS швидкі, а час до першого байта великий — думає сервер. Усі чотири числа малі, а сторінка все одно гальмує — вузьке місце вже на клієнті, у рендері чи в JavaScript, і далі треба йти в панель мережі й у профайлер. Такий розклад займає секунди й перетворює «щось гальмує» на конкретний напрямок пошуку.
Тест падає по таймауту. Чому не можна просто підняти таймаут?
Бо спершу треба відповісти, чий саме таймаут спрацював — вони живуть на різних рівнях і означають різне. Обмеження на встановлення зʼєднання в клієнті стосується лише фази підключення й нічого не каже про швидкість відповіді. Обмеження на читання відповіді закриває очікування даних. Шлюз, який не дочекався бекенда, віддає клієнту 504 — це вже чужий таймаут, і в конфігурації тесту він не лікується взагалі. І окремим шаром стоять таймаути тест-фреймворку: на навігацію, дію, перевірку й тест цілком. Підняти число, не зрозумівши рівня, означає сховати проблему: тест не стане стабільним, він стане повільним, а швидке падіння перетвориться на довге зависання, яке зʼїдає час прогону і все одно колись почервоніє.
Які запити безпечно повторювати автоматично і хто ще ретраїть, крім вашого коду?
Повторів на шляху більше, ніж здається, і вони багаторівневі. Найнижче TCP сам перевідправляє втрачені сегменти — цього з HTTP не видно взагалі. Вище ретраять HTTP-клієнти й проксі, зазвичай на 502, 503 і 504, причому пауза між спробами навмисно збільшується () — інакше повтори добʼють сервіс, який і так лежить під навантаженням. Відповідь на «чи безпечно повторювати» дає : GET, HEAD, OPTIONS, TRACE, PUT і DELETE лишають стан сервера однаковим після одного й після кількох однакових викликів, а POST, PATCH і CONNECT — ні. Звідси конкретний : автоматичний повтор POST, що створює замовлення, здатен породити дубль — і на такий дубль потім заведуть баг у продукт, хоча винен у клієнті. Тому шар ретраїв у тестовому фреймворку теж має бути свідомим: повторювати сценарій, що створює сутності, безпечно лише тоді, коли дані сценарію унікальні на кожній спробі.
Три кейси про те, як від симптому дійти до фази. Перший — , що червоніє лише на одному стенді, і розклад часу по шарах замість гадання. Другий — класика «в curl 200, у браузері нічого», де винен запит, якого в curl не існує. Третій — про те, як не перетворити діагностику на підняття .
Кейс 1. Червоно лише на CI: розклад часу замість гіпотез
Сюїта локально зелена, на падає з таймауту навігації. Спокуса очевидна — підняти таймаут і забути. Правильний перший крок інший: зʼясувати, яка фаза зʼїла час. Найдешевше це робиться прямо з ранера, без браузера й фреймворку.
# запускаємо з тієї ж машини, де падає тест
curl -sS -o /dev/null \
-w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
https://staging.example.com/api/health
Усі чотири таймінги відлічуються від старту запиту, а не від кінця попередньої фази. Тому саму фазу видно лише як різницю сусідніх чисел: TLS коштував time_appconnect мінус time_connect, а сервер думав time_starttransfer мінус time_appconnect. Порівняння двох прогонів — з ранера й з ноутбука — одразу показує, де саме розходяться середовища.
| Що видно в розкладі | Фаза-підозрюваний | Дешева перевірка |
|---|---|---|
великий time_namelookup, решта нормальна | резолвінг імені | звірити, куди резолвиться домен на ранері; заглянути у файл hosts образу |
резолвінг швидкий, time_connect довгий або обривається | транспорт і мережа до хоста | доступність порту з ранера, проксі й фаєрвол корпоративної мережі |
зʼєднання є, падіння на time_appconnect | TLS-рукостискання | термін дії сертифіката, збіг імені хоста з іменем у сертифікаті |
усе швидко, крім time_starttransfer | сервер готує відповідь | навантаження стенда, повільний запит до бази, холодний старт |
| усі чотири числа малі, а тест усе одно падає | клієнт: рендер або JavaScript | панель Network і профайлер у браузері, не мережа |
Три висновки, які економлять пів дня.
- «Падає лише в одному оточенні» майже завжди починається з імені, а не з коду. Запис у файлі
hostsна ранера тихо відправляє тести на інший стенд, і жодна перевірка коду цього не покаже. Так само працює недавня зміна IP: старий запис живе рівно стільки, скільки дозволяє TTL, а типовий орієнтир тут — порядок днів, тож «уже ж перевикотили» нічого не гарантує. - Сертифікат ламає тест ще до першого HTTP-запиту. Самопідписаний або прострочений сертифікат на , домен, що не збігається з іменем у сертифікаті, — і застосунок про ваш запит навіть не дізнається. Вимикач перевірки на кшталт
ignoreHTTPSErrorsтут доречний, але свідомо і лише для тестових доменів: на проді він робить тест сліпим до реальної . - Числа з різних інструментів не складаються. Таймінг
curlдо першого байта і фазаWaiting (TTFB)у DevTools міряють різні відрізки: перший тягне в себе резолвінг і , друга рахується від уже відправленого запиту. У тікет варто писати не лише число, а й чим його зняли.
Кейс 2. «У curl 200, у браузері порожньо»: два записи на один виклик
Ручний тестувальник каже, що форма не зберігає дані, бекендер показує успішний curl і закриває тікет. Обидва праві — просто дивляться на різні запити. Браузер перед POST з application/json шле окремий OPTIONS, і якщо той не пройшов, основний виклик не полетить узагалі: бекенд його не побачить, а в панелі мережі буде червоний OPTIONS і жодного POST.
Відтворення руками займає хвилину — треба повторити саме preflight, а не основний запит:
curl -i -X OPTIONS https://api.example.com/v1/orders \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type'
Дивимося три речі: статус має бути в діапазоні 200–299, у відповіді мають бути дозволені метод і заголовок, а — вказане конкретно, якщо основний виклик іде з обліковими даними. Характерна знахідка тут — 401 на OPTIONS: preflight принципово не несе облікових даних, тож будь-яка авторизація, повішена на цей маршрут, гарантовано його завалить.
Той самий сюжет ламає й автотести, тільки тихіше. Очікування, написане надто широко, чіпляється за preflight:
// крихко: перший запит за цим URL — це OPTIONS, а не збереження
const [res] = await Promise.all([
page.waitForResponse((r) => r.url().includes('/v1/orders')),
page.getByRole('button', { name: 'Зберегти' }).click(),
]);
expect(res.status()).toBe(201); // зачепилися за відповідь на OPTIONS, а не на збереження
Надійний варіант звіряє ще й метод — і не покладається на присутність preflight узагалі:
const [res] = await Promise.all([
page.waitForResponse(
(r) => r.url().includes('/v1/orders') && r.request().method() === 'POST',
),
page.getByRole('button', { name: 'Зберегти' }).click(),
]);
expect(res.status()).toBe(201);
await expect(page.getByTestId('order-id')).toBeVisible();
Що дивитися і чому:
200у консольному клієнті нічого не доводить про браузер. ЗаголовкиAccess-Control-*перевіряє виключно браузер — curl, Postman і серверні клієнти їх ігнорують. Тому «у Postman працює» не закриває тікет, а лише звужує пошук до браузерного шару.- Один логічний виклик — два записи в мережі. Прочитати їх треба обидва: провал на
OPTIONSозначає проблему конфігурації CORS, провал на основному — уже логіку застосунку. - Кешований дозвіл робить тест плаваючим. Заголовок
Access-Control-Max-Ageдозволяє серверу дозволити preflight наперед, тож у частині прогонів записуOPTIONSне буде зовсім. Очікування, побудоване на його наявності чи на порядковому номері запиту, ламається саме на цьому. - Зірочка і облікові дані несумісні. Якщо виклик іде з обліковими даними (cookie або HTTP-автентифікація), серверу потрібен
Access-Control-Allow-Credentials: trueі конкретне походження у відповіді — узагальнене значення браузер відхилить, і симптом знову виглядатиме як мовчазна відсутність даних. ЗаголовокAuthorizationламається інакше:Access-Control-Allow-Headers: *його не покриває, тож у дозволі він має стояти поіменно.
Кейс 3. «Сторінка гальмує»: тікет замість підняття таймауту
Найпоширеніша реакція на червоний тест по таймауту — збільшити число. Це не діагноз, а спосіб перетворити швидке падіння на довге зависання: тест стає повільним, а проблема лишається. Замість цього треба відповісти на два питання — яка фаза зʼїла час і чий таймаут спрацював.
Порядок дій у панелі мережі. Спершу відкрити DevTools до відтворення: запис ведеться лише поки панель відкрита, і перезняти вже пройдений прогін не вийде. Далі увімкнути дві колонки, яких у дефолтному наборі немає — Protocol і Connection ID. Перша відповідає, по HTTP/1.1 чи по HTTP/2 пішов трафік, друга показує, чи товпляться запити в одному зʼєднанні. Потім прочитати розкладку Timing проблемного запиту.
| Що велике в Timing | Що це означає | Наступний крок |
|---|---|---|
Queueing / Stalled | запит навіть не відправлено — чекає вільного зʼєднання | подивитись Protocol: на HTTP/1.1 діє межа паралельних зʼєднань на походження, на HTTP/2 гіпотеза відпадає |
DNS Lookup | гальмує резолвінг імені | повторити розклад із кейсу 1, перевірити резолвер оточення |
Initial connection | довге встановлення зʼєднання разом з узгодженням шифрування | мережа до хоста, стан TLS-конфігурації стенда |
Waiting (TTFB) | думає сервер | тікет на бекенд: конкретний ендпоінт, число, спосіб виміру |
Content Download | довго їде тіло | розмір відповіді, стиснення, пагінація |
| усе мале, а сторінка порожня | справа не в мережі | рендер і JavaScript: профайлер, а не Network |
Друге питання — чий таймаут. Вони живуть на різних рівнях і лікуються в різних місцях: клієнтське обмеження на встановлення зʼєднання стосується лише фази підключення; обмеження на читання закриває очікування відповіді; 504 віддає , який сам не дочекався бекенда, і в конфігурації тесту це не лікується взагалі; а таймаути навігації, дії, перевірки й тесту — це вже рівень фреймворку. Останній рівень часто й ховає справжню картину: тест валиться на очікуванні елемента, хоча зламалося зʼєднання двома фазами раніше.
І типова причина «непояснюваного» таймауту в самому тесті — очікування часу замість очікування факту:
// погано: змагаємося з реальним часом завантаження
await page.goto('/orders');
await page.waitForTimeout(3000);
await expect(page.getByRole('row', { name: 'Some order name' })).toBeVisible();
// добре: чекаємо на подію, яка справді щось означає
await page.goto('/orders');
await page.waitForResponse((r) => r.url().includes('/api/orders') && r.ok());
await expect(page.getByRole('row', { name: 'Some order name' })).toBeVisible();
Що дивитися і чому:
loadне означає «дані на місці». Подія засвідчує ресурси самої сторінки, а запити, які JavaScript робить після неї, до неї не належать. Тест, що прокинувся наload, легко випередить власні дані.pendingу панелі — не автоматично поломка. Це стан «запит іде, повної відповіді ще немає». Для стріма, SSE чи піднятого WebSocket він нормальний і вічний за задумом; заводити на нього баг — марна робота.- HAR у тікет вкладати можна, але з поправкою. Експорт типово санітизований:
Cookie,Set-CookieіAuthorizationз нього прибрано. Це добре для безпеки й погано для відтворення сценаріїв з авторизацією — про відсутність цих заголовків у файлі варто попередити в описі тікета. - Хороший тікет про продуктивність називає фазу, а не відчуття. «Сторінка гальмує» повертається з питаннями; «на
/api/ordersфаза очікування — 4,2 с у DevTools, завантаження тіла — 80 мс, протоколh2» одразу адресує проблему бекенду.
Карта фаз
- Можу назвати фази шляху запиту по порядку — від резолвінгу імені до рендеру — і пояснити, чому кожен перехід між ними це і час, і окремий режим .
- Розумію, що рендер ланцюг не завершує, а множить: під кожен CSS, скрипт, шрифт і зображення йде власний запит, часто вже по відкритому зʼєднанню.
Резолвінг імені
- Памʼятаю, що запис живе стільки, скільки дозволяє TTL, а орієнтир RFC для типового вузла — порядок днів; наслідок: після зміни IP середовища тести можуть ще довго стукати за старою адресою.
- Тримаю локальний файл
hostsі резолвер оточення у списку перших підозрюваних, коли тест падає рівно на одній машині чи одному .
Рукостискання
- Знаю, що на звичайному HTTPS-запиті рукостискань щонайменше два — TCP і TLS, — і можу сказати, чому кроків у TCP саме три.
- Можу пояснити роль SNI: імʼя хоста в
ClientHelloдає серверу з багатьма доменами вибрати потрібний сертифікат, і воно має відповідати імені всередині нього. - Розумію, чому перший запит до нового хоста дорожчий за наступні, і памʼятаю, що WebSocket відкривається як HTTP-запит
Upgrade, а ознака успіху — рівно101.
Запит і TTFB
- Знаю, що частину заголовків код підставити не може —
Cookie,Origin,Referer,Host,Content-Lengthставить сам браузер, — і щоUser-Agentу цей перелік не входить. - Можу назвати випадки, коли браузер не відправляє запит узагалі: заблокований і заборона CSP спрацьовують ще до мережевої фази.
- Розрізняю два виміри під однією абревіатурою — навігаційний і фазу очікування в DevTools — і не порівнюю їхні числа в лоб.
Кеш, редирект, preflight
- Не плутаю свіжу копію з кешу і
304, і памʼятаю, що відповідь могла прийти від service worker: рядок у панелі не доводить факту мережевого обміну. - Знаю, що
301і302дозволяють клієнту підмінити метод наGETі втратити тіло,303робить це навмисно, а зберігають і метод, і тіло лише307та308. - Можу назвати три тригери preflight і пояснити, чому авторизацію не можна вішати на маршрут
OPTIONS: облікових даних цей запит не несе.
Зʼєднання й версія протоколу
- Знаю, що шість паралельних зʼєднань на — практика браузерів для HTTP/1.x, а не норма, і що на HTTP/2 клієнту належить одне зʼєднання на хост із портом.
- Розумію, чому локальний стенд на
http://завжди йде по HTTP/1.1: версію узгоджують через ALPN усередині TLS, а браузери піднімають HTTP/2 лише поверх нього.
Рендер і очікування
- Знаю різницю
DOMContentLoadedvsloadі головне обмеження обох: жодна подія не засвідчує даних, які JavaScript тягне пізніше. - Чекаю на факт — елемент, конкретну відповідь, стан, — а не на фіксовану паузу, і можу пояснити, чому пауза однаково погана на швидкій і повільній машині.
Панель мережі й діагностика
- Памʼятаю, що трафік пишеться лише за відкритих DevTools, читаю розкладку Timing по фазах і вмикаю колонки
ProtocolтаConnection ID, яких у дефолтному наборі немає. - Вмію розкласти час запиту по фазах з через таймінги
curlі памʼятаю, що всі вони рахуються від старту, тож вартість фази — різниця сусідніх чисел. - Перш ніж піднімати , зʼясовую, чий саме спрацював: клієнтський на зʼєднання, на читання відповіді, (
504) чи таймаут тест-фреймворку; так само перед повтором дивлюся на , бо автоповторPOSTздатен породити дубль.
Квіз
Перед стартом
- Питань: 16
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
З яких фаз складається шлях запиту між Enter і сторінкою?