WebSockets і реалтайм
Зміст
Чат, у якому повідомлення зʼявляється без перезавантаження. Дашборд, де цифра змінюється сама. Статус замовлення, що переїжджає з «Готується» в «У дорозі», поки ти дивишся на екран. За всім цим — одна зміна: ініціативу віддали серверу.
Для тестувальника це окремий клас поведінки: дані приходять асинхронно, а звичне «зробив запит — перевірив відповідь» не працює, бо запиту немає. Глава — канон теми на сайті: тут повний виклад, решта глав розділу посилаються сюди.
Чому запиту-відповіді не вистачає
HTTP побудований на асиметрії ролей: клієнт встановлює зʼєднання й надсилає запит, сервер слухає й відповідає на те, що прийшло. Сервер не знає наперед, хто до нього прийде, і не має способу заговорити першим.
Доки не було нічого кращого, двобічний обмін імітували (polling): клієнт раз на N секунд питав «є щось нове?», а власні повідомлення слав окремими запитами. Специфікація WebSocket називає це прямо — зловживання HTTP. Ціна не лише в порожніх відповідях: кожна ітерація — повний цикл із заголовками, кукі й автентифікацією, а новина, що зʼявилася одразу після опитування, чекає наступного. long polling майже прибирає — запит висить, доки не буде що віддати, — але кожне повідомлення коштує закриття одного циклу й відкриття наступного. Мета WebSocket так і сформульована: двобічний звʼязок без багатьох HTTP-зʼєднань і .
Що не змінюється — ініціатором зʼєднання лишається клієнт: WebSocket не робить зі сторінки сервер, він дозволяє серверу говорити всередині вже відкритого каналу.
Рукостискання: HTTP відчиняє двері й виходить
Протокол складається з двох частин: відкривальне (handshake) і передача даних. Рукостискання навмисно зроблене звичайним HTTP-запитом GET із заголовками Upgrade: websocket, Connection: Upgrade і Sec-WebSocket-Key — щоб один порт обслуговував і HTTP-, і WebSocket-клієнтів.
Успіх означає рівно один статус — 101 Switching Protocols із заголовком Sec-WebSocket-Accept. Будь-який інший код каже, що рукостискання не завершилося й далі діє звичайна семантика HTTP: 401 — в автентифікації, 404 — немає шляху, 200 — сервер відповів як на звичайний запит і каналу не відкрив. Немає 101 — шукати кадри марно.
Після 101 HTTP закінчується: WebSocket — самостійний протокол поверх TCP, і повʼязує їх лише те, що рукостискання розпізнається HTTP-серверами як апгрейд. Наслідок великий — заголовки, кукі й автентифікація на кожне повідомлення більше не потрібні, вони пішли один раз.
TCP під ним: що надійність дає і чого не покриває
WebSocket не винаходить транспорт: він лягає поверх TCP і гарантії бере звідти. TCP дає надійний упорядкований потік байтів: втрати виявляються за номерами послідовності й контрольними сумами, а лікуються повторною передачею; заразом TCP керує потоком і перевантаженням. Зʼєднання встановлюється тристороннім рукостисканням SYN → SYN-ACK → ACK і далі ідентифікується парою сокетів.
Звідси два наслідки. Перший: порядок кадрів у межах зʼєднання гарантований транспортом. Другий, важливіший: гарантія закінчується разом зі зʼєднанням — надійність це виявлення втрат і повторна передача в живому каналі, а не обіцянка, що канал доживе до кінця сценарію. Протилежний вибір — UDP: без зʼєднання й без гарантій, зате з мінімальними накладними витратами.
Ціна не нульова: перед першим корисним байтом проходить TCP-рукостискання, а для захищеного каналу ще й TLS, і ця затримка потрапляє в кожен . Різниця з полінгом у тому, що тут вона платиться один раз: HTTP теж уміє перевикористовувати зʼєднання замість нового рукостискання, але щойно воно закрилося — наступний цикл починає з нуля.
ws і wss: чому на HTTPS-сторінці працює лише wss
Специфікація визначає дві схеми адрес: ws (типовий порт 80) і wss (443) — той самий протокол, але поверх TLS. Схема тут така сама частина URL, як http і https (див. главу URL і кодування), і різниця не косметична: wss дає ті самі три гарантії, що й HTTPS, — автентифікацію сервера, конфіденційність і цілісність. бачить IP-адресу й доменне імʼя (SNI у рукостисканні TLS), але не вміст кадрів.
Головне для практики: сторінка, відкрита по https, — захищений контекст, і ws:// із неї дає (mixed content). Поділ простий: вміст або «оновлюваний» (браузер сам переписує схему на https), або «блокований», причому блокованим вважається все, що не входить до переліку оновлюваних, — а там лише зображення, аудіо й відео, задані атрибутом src, та зображення з CSS (background-image і подібні). Тому ws:// мовчки не «оновиться» — його заблокують.
Три наслідки, що економлять годину дебагу:
- слід завжди є в — браузер пише попередження і про оновлення, і про блокування;
- на локальному стенді дефект не відтворюється:
file:і петльові адреси (http://127.0.0.1/,http://localhost/) вважаються захищеними , тож перевіряти треба на домені; - на стенді з IP-адресою замість домену блокується навіть оновлюваний запит — окреме правило, яке ловить внутрішні середовища.
Кадр — це не запит
Після рукостискання дані передаються послідовністю кадрів (frames). Кадр не має ні методу, ні шляху, ні і — головне — не має пари: сервер надсилає його, коли має що сказати. Звідси складнощі тестування — чекати на кадр за номером запиту неможливо, бо номера немає.
Асиметрія в іншому: клієнт зобовʼязаний маскувати всі кадри, які шле серверу, випадковим 32-бітним ключем, а сервер маскувати свої не має права. Це не приватність: від підслуховування захищає TLS у wss, а не маскування — по ws:// кадри йдуть відкрито. Маскування захищає проміжні вузли: щоб скрипт не сформував байти, які прийме за HTTP-запит і покладе собі в кеш.
Поверх даних є керівні кадри. Ping і Pong — вбудований keepalive: сторона, що отримала Ping, мусить відповісти Pong із тими самими даними, якщо вже не отримала кадр Close. Нюанс: браузерний API керувати цими кадрами з JavaScript не дає, тому застосунки будують власний heartbeat звичайними повідомленнями — у трафіку ти побачиш саме його.
З боку сторінки зʼєднання видно як обʼєкт зі станом readyState — CONNECTING (0), OPEN (1), CLOSING (2), CLOSED (3) — і чотирма обробниками: open, message, error, close.
Розрив, коди закриття й перепідключення
Закриття буває двох сортів, і плутати їх дорого. Нормальне — кадр Close з кодом: 1000 означає, що зʼєднання виконало своє призначення, 1001 — що сторона «йде» (сервер лягає або браузер пішов зі сторінки). Аварійне — коли кадру Close не було взагалі: обірвався Wi-Fi, помер процес на бекенді, проксі закрив неактивне зʼєднання.
Тут живе код, який найчастіше бачать у логах тестів, — 1006. Специфікація прямо каже: це зарезервоване значення, і ставити його в кадр Close заборонено — його виставляє сама реалізація, коли зʼєднання закрилося ненормально. По мережі він не передається ніколи й існує лише в обробнику close. Побачив 1006 — читай як «канал обірвався», а не «сервер щось сказав».
Відновлення — робота клієнта, а не протоколу, і вимоги специфікація адресує саме йому: після аварійного закриття клієнт SHOULD уживати (backoff), першу спробу відкладати на випадковий час, а наступні — на дедалі довші, наприклад за схемою урізаного двійкового експоненційного нарощування (truncated binary exponential backoff). Механізму «продовжити стару сесію» немає: після перепідключення це нове зʼєднання з новим рукостисканням, тож автентифікацію й підписки застосунок відновлює сам.
Перезавантаження сторінки: реалтайм починається з нуля
Чи переживе зʼєднання перезавантаження сторінки? Відповідь коротка — ні. WebSocket не є властивістю вкладки чи домену: обʼєкт зʼєднання живе в контексті виконання JavaScript конкретного документа, і разом із документом зникає.
Наслідок для реалтайму більший, ніж здається, бо стан у WebSocket не зберігається ніде, крім самого зʼєднання. Механізму «продовжити стару сесію» протокол не має — після перезавантаження це нове зʼєднання з новим рукостисканням, а отже, і нова автентифікація, і нова підписка на потрібні події.
Наша практика (не канон). Того, що зʼєднання вбиває саме перезавантаження, джерело не каже — це наш вивід із того, що обʼєкт живе в контексті виконання сторінки. Робочі правила звідси такі: після кожного page.reload() або переходу на нову сторінку стан реалтайму скидається повністю, тож розраховувати, що підписка чи отримані до перезавантаження повідомлення «доживуть» до наступного кроку тесту, не можна — після перезавантаження перевірку починають із того, що канал відкрився знову.
Сусіди по темі: SSE і полінг
WebSocket — не єдиний спосіб зробити реалтайм, і вибір між варіантами треба вміти обґрунтувати. Найближчий сусід — Server-Sent Events (SSE): однобічний канал сервер → клієнт через інтерфейс EventSource, де сервер віддає відповідь із типом text/event-stream. Назад події слати не можна.
Дві властивості SSE варто знати напамʼять. Перша: перепідключення вбудоване — обірване зʼєднання браузер відновлює сам, сервер може задати паузу полем retry, а зупиняє потік лише метод .close(). Друга: без HTTP/2 браузер тримає щонайбільше шість SSE-зʼєднань, причому ліміт рахується парою «браузер + домен» і дійсний на всі вкладки разом; HTTP/2 його не знімає, а замінює на узгоджуване число з дефолтом 100.
| Спосіб | Напрямок | Хто ініціює обмін | Перепідключення |
|---|---|---|---|
| Полінг | Клієнт питає | Клієнт, за таймером | Кожен цикл окремий |
long polling | Відповідь чекає на подію | Клієнт, після відповіді | Новий запит на повідомлення |
| SSE | Лише сервер → клієнт | Клієнт відкриває канал | Вбудоване в браузер |
| WebSocket | В обидва боки | Клієнт відкриває канал | Пише застосунок |
Межа теми: черги повідомлень, WebRTC і мобільні push сюди не входять — це окремі теми зі своєю механікою.
Що видно у вкладці Network
Мережева панель бачить не тільки HTTP: серед фільтрів типів ресурсів є кнопка WS, а DevTools від версії 99 тротлить і WebSocket-зʼєднання. У списку запитів зʼєднання — один рядок, те саме рукостискання зі статусом 101; кадри окремими рядками не додаються, тож сто повідомлень не дадуть ста запитів.
Щоб побачити кадри, клацни URL зʼєднання в колонці Name і відкрий вкладку Messages: таблиця показує останні 100 повідомлень із колонками Data, Length і Time, а напрямок передано кольором — вихідні світло-зелені, вхідні білі. Решта панелі — у главі DevTools: вкладка Network і дебаг.
Що саме перевіряє QA
Функціональні перевірки реалтайму зводяться до чотирьох питань, і всі випливають із механіки вище.
- Доставка. Чи дійшло повідомлення до потрібного клієнта — і чи не дійшло до чужого. Перевірка двома сесіями: дія в одній, наслідок в іншій.
- Порядок. Порядок кадрів гарантує транспорт, тож не в мережі, а в застосунку: чи не переставить їх фронтенд, отримавши два оновлення того самого обʼєкта підряд.
- Перепідключення. Що робить клієнт після аварійного закриття: чи пробує знову, чи нарощує паузу, чи не влаштовує шторм запитів на сервер.
- Стан після розриву. Після перепідключення це нове зʼєднання, тож предмет перевірки — відновлення стану: чи повернулися підписки, чи не загубилися повідомлення з моменту розриву, чи не показує UI застаріле як актуальне.
Правило автотестів одне: кадр не має пари, тому чекати треба на наслідок, а не на час. Жорстка пауза або зайва, або замала.
// Чекаємо на наслідок кадру в інтерфейсі
await expect(page.getByTestId('order-status')).toHaveText('У дорозі');
// Так не треба
await page.waitForTimeout(3000);
Сам трафік теж доступний із тесту, і не лише очима: інструмент автоматизації підтримує огляд, й модифікацію WebSocket «з коробки». Точка входу — подія, що спрацьовує на кожне створення сокета; далі з обʼєкта зʼєднання слухають окремі кадри в обидва боки.
page.on('websocket', (ws) => {
ws.on('framesent', (f) => console.log('→', f.payload));
ws.on('framereceived', (f) => console.log('←', f.payload));
});
Це дає перевірку не лише наслідку в інтерфейсі, а й самого обміну: чи клієнт справді підписався, чи після дії в UI пішов очікуваний кадр. Докладніше про очікування — у главі Практичні сценарії AQA: флак і синхронізація.
Наша практика (не канон). Далі — те, як це роблять команди, з якими ми працювали; окремого стандарту під це немає. Реалтайм ми вважаємо стабільним джерелом (flakiness) — це наша оцінка, а не теза джерела: повідомлення приходять непередбачувано в часі, і тест, який на це не розрахований, падає раз на N прогонів. Одне зʼєднання тримає стан підписок і чергу повідомлень, тому реалтайм-тести чутливі до ізоляції: свіжий на тест дешевший за полювання на протікання стану. Стратегій три: очікування UI-наслідків перевіряє інтеграцію з живим сервером; мокання WebSocket (сама техніка — у главі Перехоплення й мокання мережі) — поведінку фронтенду й сценарії розриву; на реальні кадри з таймінгом «на око» — найкрихкіший варіант.
Типові помилки
- «Зʼєднання не відкрилося — значить, WebSocket зламаний». Насправді статус рукостискання каже, що сталося: усе, крім
101, — звичайна HTTP-відповідь, і401чи404діагностуються як завжди. - «У Network немає кадрів — реалтайм не працює». Насправді кадрів там і не буває: у списку запитів лише рукостискання, а повідомлення — у вкладці Messages усередині зʼєднання.
- «Сервер закрив зʼєднання з кодом 1006». Насправді
1006заборонено передавати в кадріClose— його ставить локальна реалізація, коли канал обірвався без прощання. Це діагноз мережі, а не слова сервера. - «Кадри маскуються, отже, зашифровані». Насправді маскування захищає проміжні вузли від підробленого HTTP-запиту, а шифрування дає лише
wss: поws://вміст кадрів читається відкрито. - «На стенді
ws://працює — значить, і в проді запрацює». Насправді локальні адреси вважаються захищеними походженнями, тож блокування там не спрацьовує; на домені зhttpsтой самий код помре з попередженням у консолі. - «Підписка переживе перезавантаження — це ж та сама вкладка». Насправді зʼєднання живе в контексті виконання документа: після
page.reload()буде нове рукостискання, нова автентифікація й нова підписка.
Підсумок
- WebSocket — це рукостискання плюс кадри поверх TCP. HTTP потрібен рівно один раз; ознака успіху —
101, інший код означає, що каналу немає. - Ініціатива зміщується, ролі — ні. Зʼєднання відкриває клієнт, але всередині каналу сервер шле повідомлення сам — тому в кадра немає пари, і тест чекає на наслідок.
- На HTTPS-сторінці живе лише
wss.ws://із захищеного контексту блокують як змішаний контент; на локальних адресах цього не видно. - TCP гарантує порядок і доставку, доки живе зʼєднання. Розрив гарантії скасовує:
1006— обрив без прощання, а перепідключення з бекофом пише застосунок. - Стан після перепідключення — окремий предмет тесту. Нове зʼєднання не успадковує ні автентифікації, ні підписок: «канал відновився» і «застосунок працює як раніше» — різні твердження.
Можливі питання
- «Чим WebSocket відрізняється від HTTP?» Чекають механізм, а не список властивостей: одне зʼєднання замість циклу запитів, ініціатива сервера всередині каналу, відсутність заголовків на кожне повідомлення. Сильна відповідь називає й межу — HTTP потрібен для рукостискання.
- «Що відбувається під час апгрейду зʼєднання?» Перевіряють, звідки береться
101:GETізUpgrade: websocketіSec-WebSocket-Key, відповідь101 Switching ProtocolsізSec-WebSocket-Accept, далі HTTP-семантика закінчується. - «Чому на HTTPS-сторінці не працює
ws://?» Дивляться, чи звʼязуєш тему зі змішаним контентом, а не переказуєш правило. Хороша відповідь додає, чому дефект доїжджає до продакшену: на localhost блокування не відтворюється. - «Як ти тестуватимеш чат?» Часта форма питання на мідл-рівні: перевіряють структуру мислення. Сильна відповідь розкладає задачу на доставку, порядок, перепідключення й стан після розриву — і проговорює, що чекати треба на подію, а не на секунди.
- «Коли краще SSE, а коли WebSocket?» Чекають критерій: потрібен зворотний канал — WebSocket; вистачає односторонніх оновлень — SSE з вбудованим перепідключенням. Плюсом — згадка про ліміт зʼєднань SSE на всі вкладки.
Джерела
Чому запиту-відповіді не вистачає
- RFC 9110 — HTTP Semantics — ролі клієнта й сервера, схема «запит → відповідь».
- RFC 6455 — The WebSocket Protocol — полінг як зловживання HTTP (§1.1), мета протоколу (Abstract).
Рукостискання: HTTP відчиняє двері й виходить
- RFC 6455 — The WebSocket Protocol — дві частини протоколу (§1.2), заголовки апгрейду й статус
101(§1.3), незалежність (§1.7). - MDN — The WebSocket API (WebSockets) — двобічна сесія без опитування сервера.
TCP під ним: що надійність дає і чого не покриває
- RFC 9293 — Transmission Control Protocol (TCP) — упорядкований потік байтів, тристороннє рукостискання, повторна передача, пара сокетів.
- RFC 768 — User Datagram Protocol — UDP: без зʼєднання й без гарантій.
- RFC 8446 — TLS 1.3 — TLS поверх надійного транспорту й вартість рукостискання.
- MDN — Understanding latency — чому перший обмін дорожчий.
- MDN — Connection management in HTTP/1.x — зʼєднання замість нового рукостискання.
ws і wss: чому на HTTPS-сторінці працює лише wss
- RFC 6455 — The WebSocket Protocol — схеми
ws,wssі порти 80, 443 (§3). - RFC 8446 — TLS 1.3 — автентифікація сервера, конфіденційність, цілісність.
- RFC 6066 — TLS Extensions: Extension Definitions — SNI у рукостисканні.
- MDN — Mixed content — оновлюваний і блокований вміст, консоль, локальні адреси, правило про IP.
- RFC 6455 — The WebSocket Protocol — кадри, маскування клієнтом і заборона для сервера (§5.1, §5.3), мотив (§10.3),
Ping/Pong(§5.5.2, §5.5.3). - WHATWG WebSockets Standard — стани
readyStateй обробники подій.
Розрив, коди закриття й перепідключення
- RFC 6455 — The WebSocket Protocol — коди
1000,1001,1006(§7.4.1), відновлення після аварійного закриття (§7.2.3).
Перезавантаження сторінки: реалтайм починається з нуля
- MDN — The WebSocket API (WebSockets) — зʼєднання в контексті виконання JavaScript сторінки.
- RFC 6455 — The WebSocket Protocol — рукостискання відкриває кожне зʼєднання наново; механізму продовження сесії немає (§1.3, §7.2.3).
- MDN — Using server-sent events — однобічний канал,
EventSource,text/event-stream, автовідновлення, ліміт зʼєднань і HTTP/2. - RFC 6455 — The WebSocket Protocol — полінг як те, що WebSocket заміщає.
- Chrome DevTools — Network features reference — фільтр WS, , вкладка Messages, колонки й кольори кадрів.
- RFC 6455 — The WebSocket Protocol — рукостискання зі статусом
101у списку запитів.
- Playwright — Auto-waiting (actionability) — очікування придатності замість фіксованої паузи.
- Selenium — Waiting Strategies — чому програє.
- RFC 6455 — The WebSocket Protocol — обмін без кореляції «запит-відповідь».
- Playwright — Network — підтримка огляду, мокання й модифікації WebSocket, подія на створення сокета й події кадрів
framesentтаframereceived.
Що таке WebSocket і чим він відрізняється від звичайного HTTP-запиту?
WebSocket — протокол двобічного обміну повідомленнями поверх одного постійного TCP-зʼєднання. У HTTP кожна порція даних коштує окремого циклу «клієнт спитав — сервер відповів», і сервер не має способу заговорити першим: він лише реагує на те, що до нього прийшло. WebSocket знімає цю асиметрію всередині вже відкритого каналу — обидві сторони шлють дані тоді, коли їм є що сказати. Друга відмінність суто економічна: заголовки, кукі й автентифікація їдуть один раз, під час відкриття каналу, а не з кожним повідомленням. Межа теж важлива, і сильна відповідь її називає: HTTP нікуди не дівається, він потрібен рівно один раз — щоб канал відкрити, — і відкриває канал усе одно клієнт. Для тестування наслідок прямий: звична схема «зробив запит — перевірив відповідь» тут не працює, бо запиту немає.
Навіщо знадобився WebSocket, якщо реалтайм можна зробити полінгом?
(polling) реалтайм не робить, а імітує, і платить за це двічі. Перша ціна — накладні витрати: кожні N секунд іде повноцінний HTTP-цикл із заголовками, кукі й автентифікацією, а маса таких циклів повертає «нічого нового». Друга — : подія, що сталася одразу після опитування, чекатиме наступного. long polling із затримкою майже справляється — сервер притримує запит, поки не назбирається що віддати, — але й тут кожне повідомлення означає закритий цикл і відкритий наступний. Специфікація WebSocket описує цю практику різко: імітувати двобічний обмін через HTTP — зловживання протоколом, і мета нового протоколу сформульована саме як обмін в обидва боки без купи HTTP-зʼєднань і long polling.
Як виглядає відкриття WebSocket-зʼєднання? Які заголовки й статус там беруть участь?
У протоколі дві фази: спершу сторони домовляються — це (handshake), — і лише потім починається обмін даними. Домовляються звичайним HTTP-запитом GET, у якому їдуть три заголовки: Upgrade: websocket, Connection: Upgrade і Sec-WebSocket-Key. Рішення суто практичне: завдяки йому той самий порт приймає і звичайні HTTP-запити, і запити на відкриття каналу, окремої точки входу заводити не треба. Успіхом вважається рівно одна відповідь — 101 Switching Protocols із заголовком Sec-WebSocket-Accept. Після цієї відповіді HTTP-семантика на цьому зʼєднанні закінчується: далі йде самостійний протокол поверх того самого TCP.
Апгрейд повернув не 101, а 200, 401 або 404. Що це означає для діагностики?
Що каналу немає й шукати кадри марно — далі діє звичайна семантика HTTP, тобто діагностика така сама, як для будь-якого запиту. 401 — сервер не пустив через автентифікацію: перевіряй токен чи кукі, з якими сторінка відкриває зʼєднання. 404 — такого шляху на сервері немає: типово переплутаний або не те середовище. 200 означає, що сервер обробив звернення як звичайний запит і каналу не відкрив, тобто про апгрейд ніхто не домовився: перевіряй, чи запит справді пішов із заголовками апгрейду й чи цей шлях узагалі очікує WebSocket. Практичне правило для звіту про дефект: спершу назви статус рукостискання, і половина гіпотез відпаде сама. Формулювання «WebSocket зламаний» без цього статусу нічого не діагностує.
У чому різниця між ws і wss?
Це дві схеми адрес того самого протоколу: ws за замовчуванням іде на порт 80, wss — на 443 і поверх TLS. Різниця змістовна: під TLS канал отримує рівно той набір властивостей, що й HTTPS, — сервер доводить, що він саме той, за кого себе видає, вміст закритий від сторонніх, а підміна даних дорогою помітна. на шляху бачить IP-адресу й доменне імʼя (воно їде відкрито в SNI під час рукостискання TLS), але вміст кадрів для нього закритий. По ws:// вміст повідомлень читається відкрито будь-ким на шляху, тому для будь-чого, що містить токени чи дані користувача, вибір фактично один. В адресі схема стоїть на тому самому місці, що http чи https, і живе за тими самими правилами.
Чому зі сторінки на HTTPS не працює ws://?
Бо сторінка, відкрита по https, — захищений контекст, а незахищений підресурс із неї є (mixed content). Далі йде деталь, на якій усе й тримається: такий вміст ділиться на дві категорії — той, що браузер мовчки переписує на захищену схему, і той, що просто ріжеться. Перша категорія вузька: лише картинки, аудіо й відео, підключені через атрибут src, та фонові зображення з CSS. Усе інше автоматично потрапляє в другу, і WebSocket разом з ним. Тому ws:// не «підтягнеться» до wss:// мовчки — його просто заблокують. Слід завжди лишається в браузера: він пише і про автоматичне оновлення схеми, і про блокування. Правильна відповідь тут не переказує правило, а звʼязує його зі змішаним контентом.
Чому дефект зі змішаним контентом доїжджає до продакшену, хоча локально все працювало?
Бо локальні адреси браузер вважає захищеними : file:, а також петльові http://localhost/ і http://127.0.0.1/. На стенді розробника блокування просто не вмикається, і той самий код виглядає робочим. Тому перевіряти змішаний контент треба на середовищі з реальним доменом і https, а не на локальному запуску. Є ще окрема пастка внутрішніх середовищ: коли стенд відкривають за IP-адресою замість доменного імені, блокується навіть той запит, який зазвичай був би оновлюваним. Практичний висновок для QA: «на localhost працює» — не аргумент у тікеті про реалтайм, і в чеклист середовищ варто закладати перевірку саме на домені.
Що таке кадр і чому до нього не можна застосувати схему «запит-відповідь»?
Коли рукостискання позаду, обмін іде окремими кадрами (frames). Кадр не несе службових атрибутів HTTP — у ньому немає ні методу, ні шляху, ні коду відповіді, — а найважливіше те, що йому нічого не відповідає у зворотний бік: сервер відправляє кадр з власної ініціативи, а не тому, що його про це попросили. Звідси головна складність тестування реалтайму: немає номера запиту, за яким можна дочекатися «своєї» відповіді. Наслідок для автотестів — чекати треба на спостережуваний наслідок кадру (змінився статус на екрані, зʼявилося повідомлення в списку), а не на сам факт обміну й тим паче не на секунди. Це ж пояснює, чому реалтайм-тести так легко стають flaky, коли в них зашивають фіксовану паузу.
Навіщо клієнт маскує кадри й чи означає це, що вони зашифровані?
Не означає — це найпоширеніша плутанина в темі. За специфікацією клієнт накладає на кожен кадр, що йде до сервера, випадковий 32-бітний ключ, а серверу така операція заборонена. Мета — не приватність, а захист проміжних вузлів: маскування не дає скрипту сформувати такі байти, які помилково прочитає як HTTP-запит і, наприклад, покладе собі в кеш. Від підслуховування захищає TLS, тобто wss, а не маскування: по ws:// вміст кадрів іде відкрито, попри всі ключі. Асиметрія тут теж змістовна — підробки створює саме браузерний клієнт, тому вимога адресована йому.
Що таке Ping і Pong у WebSocket і чому в трафіку продукту частіше видно власний heartbeat?
Ping і Pong — керівні кадри, вбудований механізм підтримання зʼєднання живим. Отримавши Ping, сторона зобовʼязана повернути Pong з ідентичним вмістом — виняток один: якщо до неї вже прийшов кадр Close. Нюанс, який відрізняє відповідь «з практики» від переказу специфікації: браузерний API не дає керувати цими кадрами з JavaScript. Тому команди роблять keepalive власноруч — звичайними повідомленнями за таймером, — і саме їх видно у вкладці з кадрами. Практичний наслідок: регулярні однакові повідомлення в трафіку — часто не логіка продукту, а keepalive, і на «кількість отриманих повідомлень» об нього спотикаються.
Які стани має WebSocket-обʼєкт на клієнті?
Зʼєднання з боку сторінки видно як обʼєкт із властивістю readyState, що приймає чотири значення: CONNECTING (0), OPEN (1), CLOSING (2), CLOSED (3). До них додаються чотири події, на які вішають обробники: open, message, error і close. Для тестування корисні дві речі. Перша: між створенням обʼєкта й станом OPEN є проміжок, тому «сторінка завантажилась» не дорівнює «канал відкритий» — саме тут живе частина гонок у ранніх асертах. Друга: код закриття приходить в обробник close, тому саме звідти беруть діагностику, коли зʼєднання впало.
Що означає код закриття 1006 і чим він відрізняється від 1000 та 1001?
1000 і 1001 — коди нормального закриття, вони справді приїжджають у кадрі Close: перший означає, що зʼєднання виконало своє призначення, другий — що сторона «йде» (сервер вимикається або користувач пішов зі сторінки). 1006 — інша природа: це зарезервоване значення, яке заборонено ставити в кадр Close, і по мережі воно не передається ніколи. Його виставляє локальна реалізація на клієнті, коли зʼєднання закрилося ненормально — тобто прощального кадру не було взагалі. Типові причини: зникла мережа, помер процес на бекенді, проксі закрив зʼєднання, яке довго мовчало. Тому фраза «сервер закрив нас із кодом 1006» некоректна: це діагноз стану каналу, а не слова сервера, і шукати треба мережу й інфраструктуру.
Хто відповідає за перепідключення і що таке backoff?
Протокол цим не займається — перепідключення пише застосунок. Специфікація адресує клієнту рекомендацію: після аварійного закриття не ломитися одразу, а нарощувати паузу (backoff), причому перший повтор зсувати на випадкову величину, а кожен наступний відкладати довше — класична схема тут зветься урізаним двійковим експоненційним нарощуванням. Сенс саме в цьому: якщо сервер щойно впав, синхронний натовп клієнтів, які повторюють спробу однаково й одразу, доб'є його ще раз. Для QA це окремий предмет перевірки, а не деталь реалізації: дивимося, чи клієнт узагалі пробує знову, чи інтервали ростуть і чи немає шторму запитів у мережевій панелі. Випадкова складова в першій паузі — теж частина правильної відповіді, бо без неї всі клієнти прокидаються синхронно.
Що успадковує нове зʼєднання після перепідключення?
Нічого. Механізму «продовжити стару сесію» у протоколі немає: після розриву відбувається повне нове рукостискання, і це інше зʼєднання. Отже, автентифікацію, підписки на канали чи кімнати, стан «на що я підписаний» відновлює сам застосунок — і саме тут ховаються найдорожчі дефекти реалтайму. Перевіряти треба не факт «сокет знову живий», а поведінку продукту: чи ожили підписки, чи не випали з історії повідомлення, надіслані в момент обриву, чи не видає інтерфейс старі дані за свіжі. «Зʼєднання відновилося» і «застосунок працює як раніше» — два різні твердження, і тест має перевіряти друге.
Які гарантії WebSocket отримує від TCP і де вони закінчуються?
WebSocket не винаходить транспорт, а лягає поверх TCP і бере гарантії звідти. Транспорт дає впорядкований і надійний потік байтів: пропажу він помічає за номерами послідовності й контрольними сумами, лікує повторною відправкою, а заразом стримує швидкість під час перевантаження; саме зʼєднання встановлюється в три кроки й далі впізнається за парою сокетів. Звідси перший наслідок: порядок кадрів у межах одного зʼєднання гарантований транспортом, тому «повідомлення переплуталися в мережі» — малоймовірна гіпотеза. Другий наслідок важливіший: гарантія живе рівно стільки, скільки живе зʼєднання. Надійність означає виявлення втрат і повторну передачу в живому каналі, а не обіцянку, що канал доживе до кінця сценарію, — тому все цікаве для QA починається на розриві. Для контрасту: UDP працює без зʼєднання й без цих гарантій, зате з мінімальними накладними витратами.
Повідомлення в чаті іноді відображаються не в тому порядку. Де шукати причину?
Насамперед — не в мережі. Порядок кадрів у межах зʼєднання забезпечує транспорт, тож найімовірніший винуватець — застосунок: фронтенд отримав два оновлення одного обʼєкта підряд і обробив їх асинхронно, наприклад дочекавшись відповіді на супутній HTTP-запит або вставивши елемент після рендера. Друга типова причина — перепідключення посеред сценарію: після нового рукостискання це вже інше зʼєднання, і те, що прийшло до розриву й після нього, може зійтися в інтерфейсі непередбачувано. Тому в тікеті варто одразу фіксувати, чи був розрив, і додавати кадри з мережевої панелі — це відрізняє «фронтенд переставив» від «сервер надіслав у такому порядку».
Коли краще SSE, а коли WebSocket?
Критерій один — чи потрібен зворотний канал. Server-Sent Events (SSE) везе дані лише в один бік — від сервера до клієнта: браузер відкриває потік через EventSource, відповідь іде з типом text/event-stream, і відправити щось назад цим самим каналом не вийде. Якщо задача — стрічка оновлень, нотифікації чи прогрес довгої операції, SSE простіший і має вбудований козир: відновлюється він сам силами браузера, а тривалість паузи сервер задає полем retry; обірвати потік назовсім може лише явний виклик .close(). Якщо клієнт має відповідати в тому ж каналі — чат, спільне редагування, ігри — потрібен WebSocket, але перепідключення тоді доводиться писати руками. Плюсом до відповіді йде обмеження, про яке часто забувають: поки з'єднання йдуть без HTTP/2, на один домен браузер дозволяє не більше шести таких потоків, і рахунок ведеться на всі вкладки разом; HTTP/2 ліміт не скасовує, а робить узгоджуваним, із типовим значенням 100.
Як подивитися WebSocket-трафік у DevTools?
У вкладці Network серед фільтрів за типом ресурсу є кнопка WS — вона лишає тільки WebSocket-зʼєднання. Ключове, що дивує новачків: у списку буде єдиний рядок, і це рукостискання з кодом 101, — а кадри туди не потрапляють зовсім: хоч тисяча повідомлень, рядок усе одно залишиться один. Щоб побачити самі повідомлення, треба клацнути URL зʼєднання й відкрити вкладку Messages: там таблиця останньої сотні кадрів із даними, довжиною й часом, а напрямок кодує колір рядка — те, що пішло від сторінки, підсвічене зеленим, вхідне лишається білим. Обмеження в сто рядків варто памʼятати під час довгих сесій: ранні кадри просто витісняються. мережі в сучасних версіях DevTools діє й на WebSocket, тож поведінку на повільному звʼязку можна відтворити звідти ж.
Як ти тестуватимеш чат?
Розкладу задачу на чотири групи перевірок, бо всі вони випливають із механіки протоколу. Доставка: повідомлення дійшло тому клієнту, якому призначене, і не дійшло сторонньому — ставиться це на двох сесіях одночасно, коли діємо в першій, а результат дивимося в другій. Порядок: два оновлення одного обʼєкта підряд не мають переставлятися в інтерфейсі, і шукати причину тут треба у фронтенді, а не в мережі. Перепідключення: після обриву клієнт має повторювати спробу з наростаючою паузою, а не влаштовувати шторм запитів. Стан після розриву: підписки повернулися, повідомлення з моменту обриву не загубилися, інтерфейс не показує застаріле як актуальне. Окремо назву дисципліну очікувань: чекаю на наслідок кадру в інтерфейсі, а не на фіксовану кількість секунд, бо жорстка пауза або зайва, або замала.
Реалтайм-тести зелені поодинці й падають у паралельному прогоні. З чого почнеш?
З ізоляції стану, а не з . Одне WebSocket-зʼєднання тримає підписки й чергу повідомлень, тому реалтайм-тести особливо чутливі до того, що залишив по собі попередній тест: залогінений користувач, відкрита кімната, недоставлені повідомлення. Свіжий на кожен тест зазвичай дешевший за полювання на протікання стану. Друга підозра — спільні дані: два тести працюють з тією самою кімнатою чи тим самим користувачем і бачать чужі повідомлення як свої. Третя — очікування: якщо в тесті є фіксована пауза, під навантаженням у CI вона просто перестає вистачати, і тоді падіння виглядає випадковим, хоча причина детермінована. Збільшення таймаута тут лікує симптом і ховає справжню гонку.
Три кейси з робочого життя QA: скарги «реалтайм не працює» за мережевою панеллю, тест доставки повідомлення двома паралельними сесіями в Playwright і перевірка того, що застосунок переживає розрив каналу. Скрізь — що дивитися і який висновок з цього випливає.
Кейс 1. Тріаж: «чат не оновлюється» — за пʼять хвилин у DevTools
Приходить скарга: повідомлення зʼявляється лише після перезавантаження сторінки. Перед тим як писати тікет на бекенд, треба відповісти на три питання по черзі — і кожне відсікає половину гіпотез.
Питання 1: канал узагалі відкрився? Відкрий Network, увімкни фільтр WS і перезавантаж сторінку. Зʼєднання займе один рядок — це , і в колонці статусу має стояти 101.
Name Status Type Time
socket?token=eyJhbGci 101 websocket Pending
Питання 2: якщо не 101 — що саме відповів сервер? Тут працює звичайна діагностика HTTP, бо рукостискання і є звичайний HTTP-запит.
Питання 3: якщо 101 є — чи ходять кадри? Клацни на URL зʼєднання й відкрий вкладку Messages. Памʼятай про дві особливості панелі: у списку запитів кадри рядками не зʼявляються (сто повідомлень не дадуть ста запитів), а сама таблиця тримає лише останню сотню — під час довгої сесії ранні кадри витісняються.
| Що бачиш | Найімовірніша причина | Наступний крок |
|---|---|---|
| Запиту WS немає взагалі, у консолі попередження про змішаний контент | Сторінка на https відкриває ws:// — браузер блокує | Перевірити адресу сокета в конфізі середовища; на localhost це не відтворюється |
401 замість 101 | Токен не доїхав або протух | Подивитися, чим авторизується сокет: кукі, токен у query, заголовок |
404 замість 101 | Не той шлях або не те середовище | Звірити URL сокета з тим, що очікує бекенд |
200 замість 101 | Про апгрейд ніхто не домовився — сервер відповів як на звичайний запит | Перевірити, чи запит пішов із заголовками апгрейду і чи цей шлях узагалі WebSocket-ендпоінт |
101 є, у Messages порожньо в обидва боки | Канал відкрито, але застосунок не підписався на події | Дефект фронтенду: підписка не відправлена |
| Ідуть лише однакові короткі кадри за таймером | Це heartbeat застосунку, а не корисні дані | Шукати, чому сервер не шле оновлення для цього користувача |
В обробнику закриття код 1006 | Канал обірвався без прощального кадру | Дивитися мережу, таймаути проксі, перезапуски на бекенді |
Код 1001 | Сторона «йде»: деплой, перезапуск, вихід зі сторінки | Звірити час із вікном релізу |
Дві пастки цього тріажу. Перша: 1006 не приходить від сервера — його ставить сама реалізація на клієнті, тому фраза «сервер закрив зʼєднання з кодом 1006» у тікеті одразу веде розслідування не туди. Друга: браузер не «підтягне» ws:// до wss:// мовчки — WebSocket належить до блокованого , а не до оновлюваного, тож слід шукати в , а не в мережі.
Кейс 2. Playwright: доставка повідомлення двома сесіями
Головна перевірка реалтайму — дія в одній сесії дає наслідок в іншій. Робиться це двома незалежними : кожен зі своїми кукі й своїм зʼєднанням.
import { test, expect, chromium } from '@playwright/test';
test('повідомлення від Анни зʼявляється в Богдана без перезавантаження', async () => {
const browser = await chromium.launch();
// два контексти = дві незалежні сесії й два окремі WebSocket-зʼєднання
const annaCtx = await browser.newContext({ storageState: 'auth/anna.json' });
const bohdanCtx = await browser.newContext({ storageState: 'auth/bohdan.json' });
const anna = await annaCtx.newPage();
const bohdan = await bohdanCtx.newPage();
await anna.goto('https://app.example.com/chat/42');
await bohdan.goto('https://app.example.com/chat/42');
const text = `привіт ${Date.now()}`;
await anna.getByTestId('message-input').fill(text);
await anna.getByTestId('send').click();
// чекаємо на НАСЛІДОК кадру, а не на секунди
await expect(bohdan.getByTestId('message-list')).toContainText(text);
await browser.close();
});
Що дивитися і чому:
- Жодного
waitForTimeout. Кадр не має пари, тому очікувати «відповідь на запит» тут нема чого — лишається очікування спостережуваного наслідку. Фіксована пауза буває або зайвою (тест повільніший без причини), або замалою (червоний тест у CI під навантаженням), і третього стану в неї немає. - на чужій сторінці — і є перевірка доставки. Якщо перевіряти лише сторінку відправника, тест зеленітиме навіть тоді, коли сервер узагалі нікому нічого не розіслав: свій текст фронтенд часто малює локально ще до підтвердження.
- Негативна половина не менш важлива. Доставка «потрібному клієнту» означає й «не чужому»: третій контекст, відкритий на іншій кімнаті, після цієї дії має лишитися без нового повідомлення. Асерт на відсутність пишеться через
notі потребує явного очікування стабільного стану, інакше він проходить просто тому, що кадр ще не встиг прийти. - Унікальний текст на кожен прогін. Реалтайм-тести особливо чутливі до чужого стану: одне зʼєднання тримає підписки й чергу повідомлень, тож повідомлення від попереднього прогону легко приймається за своє. Таймстемп у тексті прибирає цей клас хибних зелених.
- Свіжі контексти замість спільного профілю. Ізоляція тут дешевша за розслідування: два тести, що працюють з тією самою кімнатою під тим самим користувачем, бачитимуть кадри один одного.
Кейс 3. Розрив каналу: перевіряємо не «зʼєднання відновилося», а «застосунок працює як раніше»
Найдорожчі дефекти реалтайму живуть на перепідключенні. Після обриву відбувається повне нове рукостискання: це інше зʼєднання, яке не успадковує ні автентифікації, ні підписок, — відновлює їх сам застосунок. Розрив у тесті імітується переведенням контексту в офлайн.
test('після розриву каналу оновлення статусу знову доходять', async ({ page, context }) => {
// підписку ставимо ДО навігації: канал відкривається під час завантаження сторінки
const wsPromise = page.waitForEvent('websocket');
await page.goto('https://app.example.com/orders/1001');
// фіксуємо факт відкриття каналу й ловимо його закриття
const ws = await wsPromise;
const closed = new Promise<void>((resolve) => ws.on('close', () => resolve()));
await context.setOffline(true);
await closed;
await context.setOffline(false);
// головний асерт — не «сокет живий», а поведінка продукту
await expect(page.getByTestId('order-status')).toHaveText('У дорозі');
await expect(page.getByTestId('connection-banner')).toBeHidden();
});
Коли треба відтворити те, чого на живому сервері на замовлення не викличеш — дубль повідомлення, оновлення в неправильному порядку, мовчання після реконекту, — канал підміняють на рівні тесту. Маршрут реєструють до page.goto(): зʼєднання відкривається під час завантаження сторінки й повз пізніше поставлений маршрут просто проскочить.
await page.routeWebSocket('wss://api.example.com/socket', (ws) => {
ws.onMessage(() => {
// сервер відповідає рівно тим, що потрібно сценарію
ws.send(JSON.stringify({ type: 'order.status', value: 'У дорозі' }));
});
});
Що дивитися і чому:
- Перевіряти треба стан, а не факт зʼєднання. «Сокет знову живий» і «продукт поводиться як до обриву» — це різні речі. Предмет асерту: чи повернулися підписки, чи не загубилися оновлення з моменту обриву, чи не показує інтерфейс застаріле як актуальне. Зелений
readyStateцього не доводить. - Шторм перепідключень видно в панелі. Правильний клієнт нарощує паузу між спробами й перший інтервал робить випадковим; ряд однакових спроб через рівні короткі проміжки — готовий дефект, бо в проді такі клієнти доб'ють сервер, що тільки піднімається. Це перевірка не «на око»: рядки спроб у Network мають розходитися в часі.
- Код закриття підказує сценарій. Примусовий офлайн — це саме аварійне закриття: прощального кадру немає, тож в обробнику
closeбуде1006. Код1001приходить тоді, коли інша сторона встигла попрощатися — , перезапуск, вихід зі сторінки. Для тесту стану різниці немає, для звіту про дефект — є. - перевіряє фронтенд, а не інтеграцію. Підміна каналу дає керовані сценарії розриву, але з нею тест уже нічого не каже про реального сервера. Розумний набір комбінує: живий канал для основних сценаріїв доставки, мок — для розривів, дублів і поламаного порядку.
- Асерти безпосередньо на кадри — найкрихкіший варіант. Вони привʼязуються і до формату повідомлень, і до таймінгу одночасно, тож ламаються від будь-якої зміни контракту. Читати кадри корисно для діагностики, ставити на них головний асерт функціонального тесту — ні.
Навіщо WebSocket і що саме він змінює
- Можу пояснити, чому схеми «запит — відповідь» не вистачає для реалтайму: сервер не має способу заговорити першим, а зміщується лише ініціатива всередині каналу — відкриває зʼєднання все одно клієнт.
- Знаю ціну (polling) і
long polling: порожні цикли із заголовками, кукі й автентифікацією, до наступного опитування, а вlong polling— закриття циклу на кожне повідомлення.
Рукостискання й апгрейд
- Памʼятаю склад (handshake): звичайний
GETіз заголовкамиUpgrade: websocket,Connection: Upgrade,Sec-WebSocket-Key— і що єдина ознака успіху це101разом ізSec-WebSocket-Accept, після чого HTTP на зʼєднанні вичерпується й заголовки з кожним повідомленням більше не їдуть. - Можу пояснити, що будь-який інший код читається за звичайною семантикою HTTP:
401— автентифікації,404— немає шляху,200— про апгрейд ніхто не домовився, і кадрів у всіх трьох випадках не буде.
ws проти wss і змішаний контент
- Знаю різницю схем:
wsза замовчуванням іде на порт 80,wss— на 443 і поверх TLS, тобто отримує набір властивостей HTTPS: підтверджену особу сервера, закритий від сторонніх вміст, помітну підміну даних. - Можу пояснити, чому з
https-сторінкиws://не працює: це (mixed content), причому не оновлюваний, а блокований — і чому дефект не видно наlocalhost, який вважається захищеним .
Кадри, маскування й keepalive
- Розумію, що кадр (frame) не несе службових атрибутів HTTP — жодного методу, шляху чи коду відповіді — і, головне, не має пари: сервер відправляє його з власної ініціативи.
- Знаю асиметрію маскування — клієнт накладає на кожен свій кадр випадковий 32-бітний ключ, серверу така операція заборонена — і не плутаю його із шифруванням: від підслуховування рятує лише
wss. - Памʼятаю, що
Ping/Pong— вбудований keepalive із обовʼязковою відповіддю тими самими даними, але браузерний API ними не керує, тому в трафіку видно власний heartbeat застосунку. - Знаю чотири стани
readyState(CONNECTING,OPEN,CLOSING,CLOSED) і чотири події —open,message,error,close.
TCP під каналом
- Розумію, що порядок кадрів у межах зʼєднання забезпечує транспорт, але гарантія живе рівно стільки, скільки живе саме зʼєднання: розрив її скасовує.
Закриття й перепідключення
- Знаю різницю нормального закриття (кадр
Closeз кодом:1000— призначення виконано,1001— сторона йде) і аварійного, де прощального кадру не було взагалі. - Можу пояснити
1006: його заборонено ставити в кадрClose, по мережі він не ходить, його виставляє локальна реалізація — це діагноз обриву, а не слова сервера. - Розумію, що перепідключення пише застосунок, а не протокол, і що правильна схема — (backoff) із випадковою першою затримкою, інакше клієнти доб'ють сервер синхронним натовпом.
- Памʼятаю, що механізму «продовжити сесію» немає: і після обриву, і після перезавантаження сторінки це нове зʼєднання з новим рукостисканням, тому автентифікацію й підписки відновлює сам застосунок, а стан реалтайму в тесті починається з нуля.
Сусіди по темі: SSE і полінг
- Знаю, що Server-Sent Events (SSE) везе дані лише в один бік — від сервера до клієнта — через
EventSource, з типом відповідіtext/event-streamі вбудованим у браузер відновленням потоку. - Можу сформулювати критерій вибору: є що слати назад — WebSocket; потрібні лише оновлення від сервера — SSE, але з памʼяттю про ліміт зʼєднань: без HTTP/2 їх шість на домен і спільні вони на всі вкладки разом, а HTTP/2 ліміт не знімає, лише робить узгоджуваним.
DevTools і що перевіряє QA
- Знаю, що у списку запитів зʼєднання займає рівно один рядок зі статусом
101, скільки б повідомлень не пройшло, а самі кадри дивляться у вкладці Messages після фільтра WS. - Можу розкласти тестування реалтайму на чотири предмети — доставка потрібному клієнту, порядок, поведінка при перепідключенні, стан після розриву — і памʼятаю, що доставку перевіряють двома сесіями.
- Знаю правило очікувань: кадр не має пари, тому тест чекає на наслідок в інтерфейсі, а не на фіксовану паузу, яка або зайва, або замала.
Квіз
Перед стартом
- Питань: 16
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Чому на самому HTTP реалтайм не будується?