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

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

    URL і кодування

    Зміст

    Один із найчастіших багів у житті тестувальника звучить приблизно так: у пошук ввели red & blue, а застосунок шукав тільки red. Або дзеркально: тест ?q=search results, а в адресному рядку стоїть ?q=search+results — асерт червоний, хоча продукт працює правильно. Обидва випадки не про пошук і не про асерт: URL лише виглядає як текстовий рядок, а насправді це структура з роздільниками, з частинами, які ніколи не залишають браузер, і з двома різними угодами про запис пробілу.

    Загублений параметр після редиректу, 404 через регістр у шляху, «якийсь CORS» між двома портами локалхоста, картинка, якої немає лише на проді, — усе це читається просто з адреси, якщо знати, з чого вона складається. Це канонічна глава теми: тут повний виклад структури URL і кодування, решта глав розділу посилаються сюди.

    URL — не рядок, а структура

    Формально URL (Uniform Resource Locator) описує RFC 3986 як ієрархічну послідовність компонентів: URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ], де authority ділиться ще на userinfo, хост і порт. Специфікацій при цьому дві: браузери реалізують живий WHATWG URL Standard, який прямо ставить за мету узгодити RFC 3986 і RFC 3987 із реальними реалізаціями й замістити їх, а поділ на URI/IRI визнає зайвим.

    КомпонентПрикладЩо робить
    СхемаhttpsВизначає тип URL і подальшу обробку; нечутлива до регістру, канонічна форма — малими літерами
    userinfouser:pass@Дані користувача перед @; у рядку запиту на сервер не їде — браузер зазвичай підставляє їх у заголовок Authorization
    Хостshop.example.comIP-літерал у квадратних дужках, IPv4 у десятковій формі або зареєстроване імʼя; регістр не має значення
    Порт:8443Відсутнє значення або 16-бітове число
    Шлях/catalog/42Здебільшого ієрархічні дані; закінчується першим ?, # або кінцем URI
    Запит?color=red&size=xlНеієрархічні дані після першого ? до # або кінця
    Фрагмент#reviewsНепрямо ідентифікує вторинний ресурс; дереференсує його виключно клієнт

    Дві деталі звідси дають більше, ніж здається. По-перше, внутрішньої структури запиту специфікація не диктує взагалі: розбиття на пари ключ=значення, зʼєднані &, — домовленість, успадкована від HTML-форм, тож «а що буде, якщо той самий параметр передати двічі» — питання до конкретного бекенда. По-друге, для «особливих» схем (http, https, ws, wss, ftp) визначено типові порти, які в серіалізації опускають: https://example.com:443/ і https://example.com/ — той самий ресурс, а https://example.com:8443/ — уже інше . Рівність двох URL узагалі визначається збігом їхніх серіалізованих форм, причому фрагмент за бажанням із порівняння виключають.

    Регістр — окрема пастка: схема й хост нечутливі до нього й нормалізуються в нижній, решта компонентів вважається чутливою, якщо схема не каже інакше. Фактична поведінка шляху залежить від сервера й файлової системи: на Linux регістр значущий, на Windows/IIS і в багатьох CDN — часто ні, а дехто просто редиректить /Catalog на /catalog.

    Довжину зверху специфікація не обмежує, зате задає нижню межу підтримки: RECOMMENDED тримати URI щонайменше у 8000 октетів. Решту вирішують сервери, і цифри того самого порядку — Apache має LimitRequestLine з дефолтом 8190 байтів, nginx віддає 414 (Request-URI Too Large), коли стартовий рядок запиту не влазить у буфер large_client_header_buffers. Ліміт на тіло на порядки щедріший (LimitRequestBody — 1073741824 байти), звідси й правило «великі дані — тілом, а не параметрами в URL».

    Що з URL їде на сервер, а що ні

    Це найпрактичніше знання глави. У HTTP клієнт розбирає URI на компоненти, звертається до сервера authority й надсилає йому дані з authority, шляху та запиту. Фрагмент у цьому переліку не згаданий: він відокремлюється від решти URI ще до звернення до ресурсу, і дереференсує його виключно клієнт, незалежно від схеми.

    https://shop.example.com:8443/catalog/42?color=red#reviews

    Схема: як зʼєднуватись

    Хост і порт: з ким зʼєднуватись

    Шлях і запит: що просимо

    Фрагмент: лишається в браузері

    Запит на сервер

    Прокрутка, роутер, ніякої мережі

    https://shop.example.com:8443/catalog/42?color=red#reviews

    Схема: як зʼєднуватись

    Хост і порт: з ким зʼєднуватись

    Шлях і запит: що просимо

    Фрагмент: лишається в браузері

    Запит на сервер

    Прокрутка, роутер, ніякої мережі

    Наслідки прямі. Фрагмента немає в логах сервера — ніколи, тож фільтрувати за ним на бекенді неможливо: зміна лише фрагмента серверного запиту не породжує. Дзеркально: userinfo у стартовому рядку запиту теж не зʼявляється — браузер зазвичай підставляє його в заголовок Authorization.

    Наша практика (не канон). Наслідок для тесту специфікація не формулює, це наш вивід із неї: очікування мережевої відповіді після кліку по якірному посиланню зависне, бо запиту не буде — чекати треба на зміну DOM або адреси.

    Абсолютні й відносні URL

    Абсолютний URL несе схему й достатньо даних, щоб знайти ресурс без жодного контексту: https://shop.example.com/cart. Відносний без контексту сенсу не має — коректний рядок URL мусить бути або відносним, або абсолютним (за яким може йти фрагмент), і відносний набуває значення лише відносно базового URL: item.html, ../cart, ?page=2.

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

    БазаВідноснийРезультат
    https://s.com/catalog/phones/item.htmlhttps://s.com/catalog/phones/item.html
    https://s.com/catalog/phonesitem.htmlhttps://s.com/catalog/item.html

    У другому рядку phones для резолвера — «файл», і він його відкидає.

    Баз при цьому дві, і плутають саме їх. Коли відносне посилання резолвить браузер, базою є URL документа. Коли відносний аргумент page.goto() резолвить Playwright, базою є baseURL з конфігурації, а не поточна адреса сторінки — тож перехід «не туди» трапляється навіть тоді, коли на екрані відкрито потрібну сторінку.

    // playwright.config.ts
    export default defineConfig({
      use: { baseURL: 'https://staging.example.com/app/' }, // слеш наприкінці не декоративний
    });
    
    // у тесті
    await page.goto('search?q=test'); // → https://staging.example.com/app/search?q=test

    Наша практика (не канон). Далі — наші робочі конвенції; окремого стандарту під них немає. baseURL зі шляхом закінчуємо слешем, інакше останній сегмент відкидається при кожному відносному переході. У самих тестах беремо шляхи від кореня (/catalog/phones): вони не залежать ні від слеша, ні від поточної адреси. І якщо одразу після навігації в трасі стоїть несподіваний 301 чи 308, першими гіпотезами перевіряємо регістр і завершальний слеш у шляху, а не «сервер тупить».

    Відсоткове кодування: що кодують і чому

    Відсоткове кодування (percent-encoding, воно ж URL-encoding) — механізм подання одного байта трійкою символів: % і дві шістнадцяткові цифри його значення. Кодують тоді, коли символ виходить за дозволений набір або вживається як роздільник. Незарезервовані символи — латинські літери обох регістрів, цифри, дефіс, крапка, підкреслення й тильда — кодування не потребують. Зарезервовані ж розділяють компоненти, тож коли самі дані конфліктують із роллю роздільника, їх кодують перед складанням URL: пошук red & blue у параметрі має стати q=red%20%26%20blue, інакше & розірве його на два параметри — і ви отримаєте баг із першого абзацу глави.

    Механізм двокроковий, і саме тут ламається інтуїція. Спершу текст перетворюється на байти (у вебі майже завжди UTF-8), і лише потім кожен байт окремо переписується як %XX: пробіл 0x20 дає %20, а одна українська літера «і» (U+0456) — аж дві трійки, %D1%96. Набір символів, що підлягають кодуванню, залежить від компонента: для фрагмента це керівні символи, пробіл і кілька спецсимволів, а в запиті додатково кодується #, бо він починає фрагмент. Самі шістнадцяткові цифри регістронезалежні — %2F і %2f кодують той самий байт, хоч при нормалізації їх приводять до верхнього регістру.

    Побічний ефект, важливий для QA: те саме кодування в безпеці працює як спосіб пройти повз фільтр. Перелік схем, які OWASP вимагає перебрати при пошуку , починається з URL-кодування й подвійного URL-кодування (%2e%2e%2f і ..%2f — це той самий ../), а дзеркальна вимога до застосунку — декодувати й канонізувати ввід перед валідацією і не декодувати двічі. Повний розбір — у розділі про безпеку; тут важливо, що «символ закодований» не означає «символ знешкоджений».

    Кодувати компонент чи цілий URL

    Задачі дві, і вони не взаємозамінні: закодувати одне динамічне значення — або привести до ладу вже зібраний URL. MDN формулює вибір прямо: encodeURI() призначений «to encode a URL as a whole, assuming it is already well-formed», а для окремого значення беруть encodeURIComponent(), «to avoid URL syntax characters in unwanted places». Механіка різниці ще прозоріша: encodeURI() навмисно не чіпає роздільники ; / ? : @ & = + $ , #, бо в цілому URL вони частина синтаксису — і саме тому він непридатний для значення.

    Третій інструмент — URLSearchParams, серіалізатор формату application/x-www-form-urlencoded. І тут зʼявляється друга угода про пробіл.

    ІнструментПробілРоздільники (&, =, #)
    encodeURIComponent()%20кодує
    encodeURI()%20лишає як є
    URLSearchParams+кодує

    У режимі application/x-www-form-urlencoded пробіл кодується не як %20, а як +; відповідно справжній плюс записується там як %2B, а в шляху + — це літеральний плюс. Сам формат кодує список пар «імʼя-значення», а стандарт відверто називає його історичною , збереженою заради сумісності. Той самий ефект видно в обʼєкті URL: для шляху він використовує %20, а searchParams серіалізує пробіл як +.

    Наша практика (не канон). Угоду про + знає парсер form-urlencoded, і звідси наш вивід — прямого твердження джерела не мають: decodeURIComponent про неї не знає й лишає плюс плюсом, тому значення запиту читають через URLSearchParams.

    Звідси два робочі правила для тесту: порівнюй декодовані значення, а не сирі рядки (?q=search+results і ?q=search%20results логічно однакові, але як рядки не збігаються), і матчинг у будуй по розібраному URL — інакше результат залежить від того, хто і як закодував значення (Перехоплення й мокання мережі).

    // асерт по розібраному URL, а не по рядку
    const url = new URL(page.url());
    expect(url.searchParams.get('q')).toBe('red & blue');
    
    // матчер маршруту теж по компонентах
    await page.route(
      (u) => u.pathname === '/api/search' && u.searchParams.get('q') === 'red & blue',
      (route) => route.fulfill({ body: '{"items":[]}', contentType: 'application/json' }),
    );

    Не-ASCII: кирилиця у шляху, у параметрах і в хості

    У шляху й запиті правило вже описане: UTF-8, потім побайтово. На дроті /статті виглядає як /%D1%81%D1%82%D0%B0%D1%82%D1%82%D1%96, а ?q=Київ — як ?q=%D0%9A%D0%B8%D1%97%D0%B2. Адресний рядок показує людську форму, у логах і в HAR лежить закодована — це та сама адреса, а не дві різні.

    Хост — окремий випадок, і поширене «у доменних іменах кодування не буває» тут неточне: граматика reg-name відсоткові октети дозволяє, саме так подають не-ASCII зареєстровані імена. Обмеження інше — відсоткове кодування в хості не використовують, якщо воно не подає послідовність UTF-8. Модальності теж різні, і їх легко переплутати: перетворення інтернаціоналізованого імені в IDNA перед пошуком імені — вимога (must), а віддавати перевагу IDNA над відсотковим кодуванням у самому URI — лише рекомендація з умовою (should, «if they wish to maximize interoperability»). Практичний наслідок один: браузерне API повертає форму xn--…, тож асерт «поточний хост дорівнює приклад.укр» провалиться, і це не баг застосунку.

    Ресурс і ендпоінт: що стоїть за шляхом

    За шляхом стоїть не файл на диску, а ресурс (resource) — ключова абстракція REST: ресурсом може бути будь-що, чому можна дати назву (документ, зображення, послуга в часі на кшталт «сьогоднішня погода в Лос-Анджелесі», колекція інших ресурсів, невіртуальний обʼєкт). Головне тут — що ресурс є концептуальним відображенням на набір сутностей, а не самою сутністю в конкретний момент: вміст за тією самою адресою може змінюватися, лишаючись тим самим ресурсом, а дії виконуються над представленням (representation) — послідовністю байтів плюс метадані. Висновок для тесту прямий: асерт спирається на інваріанти, а не на «те, що там лежало вчора».

    (endpoint) — конкретна URL-адреса, за якою до ресурсу або колекції звертаються. Оскільки шлях містить ієрархічні дані, які разом із неієрархічним запитом ідентифікують ресурс, звідси й звична конвенція: колекція — множиною, ідентифікатор — окремим сегментом шляху (/api/v1/users/42), а фільтри, сортування й — параметрами запиту. Дію задає HTTP-метод, а не URL (REST API та формати даних). І дрібниця, що коштує годин: схема й хост нечутливі до регістру, а шлях і запит — чутливі, тож тест, який складає URL із даних, ловить 404 на рівному місці саме тут. Як будувати запити в API-клієнті — тема розділу про API.

    Редиректи: що переїжджає разом з адресою

    Редирект (redirect) ініціює сервер: відповідь із кодом 3xx і заголовком Location, у якому стоїть цільова адреса; сам клас означає, що запит нормальний, але для завершення потрібна ще одна дія. Постійні редиректи (301, 308) кажуть, що стару адресу більше не слід використовувати, і MDN додає, що й читалки оновлюють її в себе; RFC 9110 обережніший — сервер це лише пропонує, і «this suggestion is usually ignored». Тимчасові (302, 303, 307) канонічною лишають початкову адресу.

    Найпрактичніше розрізнення — що стається з методом і тілом.

    КодМетод і тіло при переході
    301, 302GET лишається GET; інші методи клієнт може підмінити на GET, і тіло тоді втрачається
    303Метод навмисно змінюється на GET, тіло втрачається (типово — після обробки форми)
    307, 308Метод і тіло не змінюються

    Модальність специфікації тут різна, і сильні кандидати це знають: нормативну заборону змінювати метод RFC 9110 прописує лише для 307, у розділі про 308 такого речення немає взагалі (норма живе в RFC 7538), а про тіло RFC 9110 не говорить у жодному з цих місць — його збереження документує саме MDN. Сам 308 — молодший код (June 2014), і специфікація застерігає, що він «might not be recognized everywhere».

    Тепер про параметри. Location несе цільовий URL, тож рядок запиту переїде рівно тоді, коли сервер його туди переніс; якщо ні — параметри зникають, а в тесті це виглядає як «фільтр сам скинувся» або «після логіну повернуло не туди». Побачити це з коду тесту можна лише з вимкненим автопрямуванням: браузери й більшість HTTP-клієнтів ідуть за перенаправленнями самі, тож у коді видно фінальний 200, і зламана ланка ланцюга проходить мовчки (у браузерному тесті той самий ланцюг лишається видимим у панелі Network). Помітний виняток — curl, який без прапорця -L за редиректом не піде.

    Наостанок три межі. 304 Not Modified формально теж 3xx, але це не переїзд: клієнт уже має валідну збережену копію, і сервер відсилає його до неї (Кешування); 300 Multiple Choices — ручний вибір із переліку. Кожен редирект коштує зайвого HTTP-запиту, а петля зазвичай означає проблему конфігурації сервера, і той, якщо здатен її помітити, віддає 500. І є ще два способи перенаправлення, яких у ланцюзі 3xx не буде взагалі: meta-refresh у HTML і зміна адреси через DOM (HTTP статус-коди).

    Хост і порт: у що резолвиться адреса

    Хост в URL — це імʼя, яке ще треба перетворити на адресу, а порт — номер, за яким на машині знаходять конкретний сервіс. Канон теми — глава DNS, IP, порти та мережа; тут лише те, без чого не читається сам URL.

    IP-адреса веде до машини: IPv4 — це 32 біти чотирма десятковими октетами, IPv6 — 128 біт вісьмома групами до чотирьох шістнадцяткових цифр, у яких провідні нулі опускають, а один ланцюжок нульових груп згортають до ::. Саме тому IPv6-літерал в URL беруть у квадратні дужки: інакше двокрапки не відрізнити від роздільника порту. Порт — 16-бітове число, яким TCP ідентифікує сервіси застосунків і веде кілька окремих потоків між тими самими хостами; адреса разом із портом називається сокетом, а зʼєднання визначає пара сокетів. IANA ділить діапазон на well-known (0–1023), registered (1024–49151) і dynamic/ephemeral (49152–65535); типові номери — 80 (HTTP), 443 (HTTPS), 22 (SSH), 53 (DNS), 3306 (MySQL), 5432 (PostgreSQL), 6379 (Redis), 27017 (MongoDB), 8080 (dev-сервери). Для стандартних схем порт можна не писати — мається на увазі типовий.

    Окремо — loopback, бо на ньому спотикаються щодня. 127.0.0.1 і ::1 означають «ця сама машина», і пастка з localhost не в резолві імені: обидві адреси за цим імʼям доступні, а резолверам рекомендовано (should) завжди повертати loopback. Пастка — у виборі сімейства адрес: двостековий клієнт шле AAAA-запит першим і свідомо віддає перевагу IPv6. Якщо застосунок слухає лише 127.0.0.1, а клієнт пішов на ::1, зʼєднання «незрозуміло чому» не встановлюється.

    Походження: схема, хост і порт разом

    Походження (origin) — це трійка «схема + хост + порт»: дві URL належать до одного походження тоді й лише тоді, коли всі три складові збігаються. Що в трійку не входить — шлях, параметри запиту й фрагмент; відмінність лише у шляху зберігає однакове походження. Логіка проста: походження описує, з ким браузер розмовляє, а не що саме він просить, а фрагмент до того ж на сервер не їде взагалі — тож частиною цієї трійки він бути не може за побудовою.

    Далі — три наслідки, які й дають більшість «дивних» помилок:

    • Піддомен — це інший хост. app.example.com і api.example.com — різні походження, попри спільний домен другого рівня.
    • Інша схема — інше походження. http://site.com і https://site.com різні, бо різниться перший член трійки.
    • Порт має значення. https://site.com і https://site.com:443 — одне походження (типовий порт опускають), а https://site.com:8443 — уже інше.

    Останній пункт і є причиною доброї частки болю на локальних стендах: фронт на http://localhost:3000 і API на http://localhost:4000 — це два різні походження з усіма правилами, хоча «це ж один localhost». Що саме забороняє ця межа й як сервер її легально послаблює — канон глави CORS і політика одного походження.

    Змішаний контент: схема в URL як безпекова межа

    (mixed content) — це коли сторінка завантажена по HTTPS, а частину ресурсів тягне по HTTP: значок у браузері обіцяє захист, а частина трафіку йде відкрито, і такі ресурси можна прочитати або підмінити. Сучасний поділ у специфікаціях — «оновлюваний» (upgradable) і «блокований» (blockable) вміст: перший браузер має автоматично перевести з HTTP на HTTPS, другий — заблокувати. Оновлюваний запит браузер переписує зі схеми http на https сам, і якщо ресурсу там немає, запит просто зазнає невдачі — відкоту на HTTP не буде. Блокується все, що не є оновлюваним: <script src>, <link href> зі стилями, <iframe src>, а також fetch() і XMLHttpRequest.

    Дві тонкості ловлять навіть досвідчених. По-перше, до оновлюваних належать не «зображення» взагалі, а <img> із джерелом у src; те саме зображення через srcset чи <picture> стоїть у переліку блокованих. По-друге, запити, які інакше були б оновлені, блокуються, якщо хост URL — IP-адреса, а не доменне імʼя.

    Звідси головна пастка тестування: локальний стенд цього дефекту не показує, бо локальні ресурси вважаються такими, що з захищених походжень, — і file:-URL, і вміст із loopback-адрес на кшталт http://127.0.0.1/ чи http://localhost/. Перевіряти змішаний контент треба на адресі з доменним імʼям. Слід у є завжди, бо браузер попереджає і про оновлення, і про блокування, а в Playwright консоль читають через подію console — це швидший діагноз, ніж півгодини правити . Симптом при цьому не завжди виглядає як «немає картинки»: заблокований може бути fetch, і тоді сторінка лишається без даних (HTTPS, TLS і безпека).

    SPA і MPA: роль фрагмента й History API

    Односторінковий застосунок (SPA, single-page application) завантажує лише один документ і далі оновлює вміст його body через JavaScript-API, коли треба показати інший контент; переходи «між сторінками» документ не перезавантажують — змінюються тільки URL і вміст.

    Змінити адресу без походу на сервер можна двома способами, і обидва вже описані вище. Перший — фрагмент: він на сервер не їде за визначенням, тож усе після # браузер обробляє сам. Другий — History API: history.pushState() і history.replaceState() міняють URL без запиту на сервер. Важлива деталь для автотестів: подія popstate реагує лише на переміщення по історії, а самі виклики pushState/replaceState її не генерують — слухач popstate клієнтської навігації не побачить.

    Наслідок для очікувань прямий. У багатосторінковому застосунку дочекатися завершення переходу просто: є навігаційні події й подія load. У SPA ці сигнали на переходах не спрацьовують, і чекати доводиться на появу контенту. А оскільки pushState міняє адресу миттєво й нікого не питає, асерт лише на URL підтверджує тільки те, що відпрацював роутер (Асинхронне завантаження: SPA/MPA, CSR/SSR, Практичні сценарії AQA: флак і синхронізація).

    Типові помилки

    • «Тест бачить q=search+results замість search results — застосунок псує дані». Насправді + — це угода формату application/x-www-form-urlencoded про пробіл; порівнювати треба декодовані значення.
    • «Фрагмента немає в логах сервера — логування зламане». Насправді фрагмент відокремлюється до звернення до ресурсу й на сервер не їде взагалі.
    • «encodeURI() закодував рядок — значення безпечно класти в параметр». Насправді ця функція навмисно лишає роздільники & = ? # недоторканими, тож значення з & розірве запит; для значення потрібен encodeURIComponent().
    • «Після 302 наш POST дійшов з тілом». Насправді при 301/302 клієнт може підмінити метод на GET і втратити тіло; гарантію дають 307/308, а 303 змінює метод навмисно.
    • «Локально змішаного контенту немає — і в проді не буде». Насправді file: і loopback-адреси вважаються захищеними походженнями, тож локально дефект не відтворюється, а на стенді з IP замість домену такі запити ще й блокуються замість оновлення.
    • «https://example.com:8443 і https://example.com — та сама адреса, просто порт інший». Насправді порт входить у трійку походження, тож це два різні походження; а от :443 для https дійсно опускають.
    • «Слеш наприкінці baseURL — косметика». Насправді при резолвінгу відкидається все після останнього / у шляху бази, тож без слеша останній сегмент зникає й відносний перехід веде на рівень вище.

    Підсумок

    • URL — структура, а не рядок. Компоненти мають різні правила й різну чутливість до регістру, а рівність двох URL визначається збігом серіалізованих форм.
    • На сервер їдуть authority, шлях і запит; фрагмент — ніколи. Це вирішує суперечки про логи, про фільтри на бекенді й про зависле очікування після кліку по якорю.
    • Кодують байти, а не символи, і завжди перед складанням URL — спершу текст у UTF-8, потім кожен байт як %XX. Кодування значення й кодування цілого URL при цьому різні операції: encodeURIComponent() — для динамічного значення, encodeURI() — для вже зібраної адреси, URLSearchParams — для пар запиту, і саме він пише пробіл як +.
    • Відносний URL читається лише разом із базою. Резолвінг відкидає все після останнього / у шляху бази, а браузер і тест- беруть за базу різні речі: перший — URL документа, другий — baseURL з конфігурації.
    • Походження — це трійка «схема + хост + порт». Шлях, запит і фрагмент у неї не входять, зате інший порт чи піддомен роблять походження іншим.

    Можливі питання

    • «З яких частин складається URL і що з них надсилається на сервер?» Чекають перелік компонентів і чітке «фрагмент лишається на клієнті»; сильна відповідь додає чому — він відокремлюється ще до звернення до ресурсу.
    • «Навіщо потрібне відсоткове кодування і які символи кодують?» Перевіряють розуміння ролі роздільників. Слабка відповідь — «щоб не було пробілів»; сильна — «щоб дані не конфліктували з роллю зарезервованих символів», з прикладом & у значенні параметра.
    • «Чим encodeURI() відрізняється від encodeURIComponent() Класика, на якій видно практику: перший навмисно не чіпає синтаксичні символи URI, тож придатний для цілого URL, а не для значення.
    • «Чому в одному місці пробіл %20, а в іншому + Дивляться, чи знає кандидат про формат application/x-www-form-urlencoded: справжній плюс у ньому — %2B, а decodeURIComponent про цю угоду не знає.
    • «Що таке origin і чи входить у нього шлях?» Питання-місток до CORS. Відповідь: трійка «схема + хост + порт», шлях, параметри й фрагмент не входять; хороший кандидат наводить приклад із різними портами на локалхості.
    • «У чому різниця між 301, 302, 303, 307 і 308 Перевіряють дві осі — постійність і збереження методу з тілом. Сильна відповідь називає 303 як навмисну зміну методу на GET і згадує, що редиректи перевіряють із вимкненим автопрямуванням.

    Джерела

    URL — не рядок, а структура

    Що з URL їде на сервер, а що ні

    • RFC 3986 — URI Generic Syntax — які компоненти клієнт надсилає серверу, відокремлення фрагмента до звернення до ресурсу.

    Абсолютні й відносні URL

    • WHATWG URL Standard — коректний рядок URL як або відносний, або абсолютний, за яким може йти фрагмент.
    • RFC 3986 — URI Generic Syntax — зʼєднання шляхів: усе після останнього / у шляху бази відкидається.
    • Playwright — Test configuration — резолвінг відносного аргументу goto відносно baseURL з конфігурації.
    • MDN — Redirections in HTTP — редирект як додатковий крок одразу після навігації.

    Відсоткове кодування: що кодують і чому

    Кодувати компонент чи цілий URL

    • MDN — encodeURI() — перелік символів, які функція не екранує, і правило вибору між encodeURI та encodeURIComponent.
    • MDN — encodeURIComponent() — кодування окремого значення разом із роздільниками й пробілом як %20.
    • WHATWG URL Standard — формат application/x-www-form-urlencoded, пробіл як +, серіалізація пар «імʼя-значення».
    • MDN — URL (Web API) — різниця між кодуванням шляху й серіалізацією searchParams.
    • Playwright — Network — матчинг запитів у перехопленні мережі по розібраному URL.

    Не-ASCII: кирилиця у шляху, у параметрах і в хості

    • RFC 3986 — URI Generic Syntax — UTF-8 перед побайтовим кодуванням, дозвіл відсоткових октетів у reg-name, модальності must для IDNA перед пошуком імені й should для форми в URI.
    • WHATWG URL Standard — форма хоста, яку віддає браузерне API.

    Ресурс і ендпоінт: що стоїть за шляхом

    Редиректи: що переїжджає разом з адресою

    • MDN — Redirections in HTTPLocation і клас 3xx, постійні й тимчасові редиректи, поведінка методу й тіла для 301/302/303/307/308, 304 і 300, ціна редиректу, петля, meta-refresh і JS-редирект.
    • RFC 9110 — HTTP Semantics — семантика класу 3xx, «this suggestion is usually ignored» для 301, нормативна заборона зміни методу лише для 307, вік 308.
    • curl — manual pagecurl без -L за редиректом не йде.

    Хост і порт: у що резолвиться адреса

    Походження: схема, хост і порт разом

    • MDN — Same-origin policy — походження як трійка «схема/хост/порт», що в неї не входить, піддомен як інший хост.
    • WHATWG URL Standard — порт як 16-бітове значення й типові порти особливих схем, які в серіалізації опускають.
    • RFC 6454 — The Web Origin Concept — походження як межа безпеки, до якої входить порт.

    Змішаний контент: схема в URL як безпекова межа

    • MDN — Mixed content — поділ на оновлюваний і блокований вміст, <img src> проти srcset/<picture>, блокування при IP-адресі в хості, локальні ресурси як захищені походження, попередження в консолі.
    • Playwright — class Page — подія console як спосіб побачити попередження браузера з тесту.

    SPA і MPA: роль фрагмента й History API

    • MDN Glossary — SPA (Single-page application) — один документ і оновлення вмісту через JavaScript-API замість перезавантаження.
    • MDN — History APIpushState/replaceState без запиту на сервер і те, що вони не генерують popstate.
    • MDN — Window: load event — подія завершення завантаження документа як сигнал навігації документа.
    • Playwright — class Page — стани завантаження сторінки й очікування контенту замість навігаційних подій.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.