REST API та формати даних
Зміст
Кнопка «Зберегти» — це не подія в базі даних, а HTTP-запит до ресурсу за конкретною адресою: метод каже, що зробити, тіло несе дані, повертає вердикт. Поки цей рівень невидимий, баги виглядають однаково — «щось не працює». Коли він видимий, ви за пів хвилини кажете, чиє це: 422 із поясненням у тілі — це валідація бекенда, 200 із порожнім масивом — це дані, а не верстка, а 415 — це ваш власний тест, який надіслав JSON туди, де чекали форму.
Це канонічна глава теми: тут повний виклад моделі ресурсу й форматів обміну, і решта глав розділу посилаються сюди. Вона ж — вхід у розділ про API-тестування: усе, що нижче, — не «як тестувати API», а грамотність щодо самої платформи, без якої перевірки не мають на що спиратися.
Клієнт-сервер: рамка, у якій усе це живе
Клієнт — програма, яка встановлює зʼєднання й надсилає запити; сервер — програма, яка їх слухає й формує відповіді. За цим поділом стоїть інженерний принцип розділення відповідальностей: інтерфейс відокремлено від зберігання даних, що покращує інтерфейсу між платформами й масштабованість серверної частини.
Запит складається з методу, шляху, заголовків і тіла; відповідь — зі статус-коду, заголовків і тіла. Звична формула «один запит — одна відповідь» точна лише щодо фінальної відповіді: специфікація прямо «zero or more "interim" (non-final) responses» зі статусами класу 1xx, за якими йде рівно одна фінальна. Семантика при цьому не залежить від версії протоколу — вимоги сформульовані так, щоб повідомлення можна було ретранслювати між версіями без зміни змісту.
Ще одне обмеження стилю REST пояснює, чому між вами й застосунком буває більше учасників, ніж здається: у багатошаровій системі компонент не «бачить» далі того шару, з яким взаємодіє. Тому на шляху запиту спокійно живуть , балансувальники й CDN, а клієнт про них не знає — і саме тому відповідь інколи приходить не від того, від кого ви очікували.
REST — це набір обмежень, а не формат даних
REST (Representational State Transfer) — не протокол і не стандарт, а архітектурний стиль для розподілених гіпермедійних систем; його описав Рой Філдінг (Roy Fielding) у докторській дисертації 2000 року. Стиль задає обмеження на взаємодію компонентів, і система, що їх дотримується, вважається RESTful.
Обмежень шість:
- client-server — розділення відповідальностей: інтерфейс окремо, зберігання даних окремо;
- stateless — кожен запит містить усе потрібне для розуміння, стан сесії лишається на клієнті;
- cache — відповідь має бути явно або неявно позначена як кешована чи ні, а кешовану клієнт має право перевикористати (плата — застарілих даних; докладніше в главі «Кешування»);
- uniform interface — центральна риса, що відрізняє REST від інших мережевих стилів; складається з чотирьох підобмежень: ідентифікація ресурсів, маніпуляція ресурсами через представлення, самоописові повідомлення й гіпермедіа як рушій стану застосунку (HATEOAS);
- layered system — компонент не бачить далі сусіднього шару;
- code on demand — єдине необовʼязкове обмеження: сервер може віддати виконуваний код.
Головне, що варто винести: жодне з обмежень не згадує JSON. Формат обміну до стилю не належить узагалі — REST задає, як влаштована взаємодія, а не в чому серіалізовані дані.
Наша практика (не канон). Далі — те, як це виглядає в реальних командах; окремої специфікації під це немає. HATEOAS реалізують рідко, і більшість «REST API» його ігнорують, а самим терміном часто називають будь-який HTTP-API з JSON-відповідями. Тому на співбесіді безпечніше казати «HTTP-API у стилі REST» і не сперечатися про чистоту: практична цінність слова REST — у передбачуваності адрес, методів і самодостатніх запитів, а не в сертифікаті відповідності.
Безстановість: кожен запит самодостатній
HTTP визначено як безстановий (stateless) протокол: семантику кожного повідомлення можна зрозуміти ізольовано, а сервер не має права припускати, що два запити на одному зʼєднанні прийшли від того самого агента, якщо саме зʼєднання не захищене й не виділене цьому агентові. Те саме обмеження формулює REST: запит мусить нести всю інформацію, потрібну для його розуміння, і не може спиратися на збережений на сервері контекст.
Мотив — масштабованість: якщо сервер не мусить памʼятати клієнтів, будь-який із десятка однакових серверів обробить будь-який запит. Другий наслідок — видимість: щоб визначити природу запиту, системі моніторингу (і вам у панелі Network) достатньо самого запиту, без відновлення історії.
Практично це означає, що «памʼять» доводиться будувати поверх протоколу, і будують її двома способами. Кукі: сервер ставить її заголовком Set-Cookie, а агент користувача сам вирішує, чи повертати пари «імʼя-значення» у заголовку Cookie наступних запитів. Токен: специфікація задає форму запиту — credentials = "Bearer" 1*SP b64token у заголовку Authorization — і вимоги до сервера, а обовʼязків на клієнтський код не покладає. Для тесту звідси випливає найдешевший спосіб авторизації: отримати сесію одним API-викликом і підкласти її в контекст, а не проклацувати форму входу в кожному сценарії.
Наша практика (не канон). Асиметрію двох механізмів джерела прямо не формулюють, але вона щоденна: кукі доїде сама, а токен поїде лише той, який поклав код застосунку. Тому «розлогінений тест» на кукі й на токені діагностується по-різному — деталі в главі «Кукі, сесії та сховище браузера».
Ресурс і ендпоінт: чому це не синоніми
Ресурс (resource) — ключова абстракція REST: ним може бути будь-що, чому можна дати назву — документ, зображення, послуга в часі («сьогоднішня погода в Лос-Анджелесі»), колекція інших ресурсів, невіртуальний обʼєкт на кшталт людини. Головне в означенні інше: ресурс — це концептуальне відображення на набір сутностей, а не сутність, що відповідає йому в конкретний момент. Вміст за тією самою адресою змінюється, а ресурс лишається тим самим.
(endpoint) — конкретна адреса, за якою до ресурсу звертаються. Ресурс — «що», ендпоінт — «де». А діють над ресурсом через представлення (representation): послідовність байтів плюс метадані, що описують його поточний або бажаний стан. Тому «формат відповіді» і «ресурс» — теж різні речі: JSON у тілі є представленням, а не самим ресурсом.
Звідки беруться звичні конвенції адрес: шлях містить ієрархічні дані, які разом із неієрархічним компонентом запиту ідентифікують ресурс. Тому колекції називають множиною, ідентифікатор ставлять окремим сегментом шляху, а фільтри, сортування й передають параметрами рядка запиту.
| Адреса | Що це |
|---|---|
/api/v1/users | колекція користувачів |
/api/v1/users/42 | конкретний користувач |
/api/v1/users/42/orders | замовлення користувача 42 (вкладений ресурс) |
/api/v1/orders?status=paid | та сама колекція замовлень із фільтром |
Дію при цьому задає метод, а не адреса. І ще одне правило, яке рятує : у межах однієї версії API поля прийнято додавати, тому тест має перевіряти потрібну йому підмножину, а не рівність усього тіла — не ламається від безпечного розширення відповіді.
Наша практика (не канон). Три речі нижче — конвенції індустрії, а не вимоги специфікацій. Дієслова в адресах (/getUser, /createOrder) вважають антипатерном, бо дію вже задає метод. Версію найчастіше вводять у шлях (/api/v1/users), рідше — заголовком чи query-параметром. Колекції віддають порціями двома способами: offset/limit дозволяє стрибнути на довільну сторінку, але «пливе», коли дані змінюються між запитами, а курсор — непрозорий маркер позиції — стабільніший, зате зазвичай ходить лише вперед-назад.
Адреса: що з URL доходить до сервера
Формально адреса — це послідовність компонентів: URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ], де authority ділиться на userinfo, хост і порт. Для API важливі три наслідки.
Перший: клієнт надсилає серверу дані з authority, шляху й запиту — фрагмент відокремлюється ще до звернення до ресурсу й обробляється виключно клієнтом. У логах API фрагмента немає ніколи. Другий: схема й хост нечутливі до регістру й нормалізуються в нижній, а шлях і запит вважаються чутливими, якщо схема не каже інакше, — звідси «404 на рівному місці» після того, як тест склеїв URL із даних. Третій: типові порти особливих схем у серіалізації опускають, тож https://api.example.com:443/v1 і https://api.example.com/v1 — та сама адреса.
Внутрішньої структури рядка запиту специфікація не диктує взагалі: розбиття на пари ключ=значення, зʼєднані &, — домовленість, успадкована від HTML-форм. Тому «а що буде, якщо той самий параметр передати двічі» — питання до конкретного бекенда, а не до стандарту. Довжину зверху теж ніхто не обмежує, зате задано нижню межу підтримки: RECOMMENDED тримати URI щонайменше у 8000 октетів. Практичні межі ставлять вебсервери — в Apache це LimitRequestLine з дефолтом 8190 байтів, а nginx на завеликий стартовий рядок віддає 414 (Request-URI Too Large). Тіло при цьому має власний ліміт, на порядки більший, — звідси й правило «великі дані передаємо тілом, а не в адресі». Повний розбір — у главі «URL і кодування».
Відсоткове кодування в шляху й рядку запиту
Відсоткове кодування (percent-encoding) — механізм подання байта трійкою символів: % і дві шістнадцяткові цифри його значення. Кодують тоді, коли символ виходить за дозволений набір або вживається як роздільник компонента. Незарезервовані символи — латинські літери, цифри, дефіс, крапка, підкреслення й тильда — кодування не потребують.
Механізм двокроковий, і саме тут ламається інтуїція: спершу текст перетворюється на байти (у вебі майже завжди UTF-8), і лише потім кожен байт окремо переписується як %XX. Пробіл 0x20 дає %20, а одна українська «і» (U+0456) — аж дві трійки, %D1%96. Набір символів, що підлягають кодуванню, залежить від компонента адреси, а самі шістнадцяткові цифри регістронезалежні: %2F і %2f кодують той самий байт.
Головна пастка для API — дві різні угоди про пробіл. У режимі application/x-www-form-urlencoded пробіл кодується як +, а не %20; відповідно справжній плюс у цьому форматі записується %2B, тоді як у шляху + — це літеральний плюс. Ті самі граблі в коді тесту: encodeURIComponent дає для пробілу %20 і кодує &, URLSearchParams серіалізує пробіл як +, а encodeURI навмисно не чіпає роздільники URI (; / ? : @ & = + $ , #) — він призначений для вже сформованого URL цілком, а для окремого динамічного значення беруть encodeURIComponent. Практичний висновок один: порівнюйте декодовані значення, а не сирі рядки, інакше ?q=search+results і ?q=search%20results дадуть червоний асерт на однакових даних.
Метод — це операція, а не частина адреси
Метод каже, що зробити з ресурсом. GET запитує передачу поточного вибраного представлення; POST просить ресурс обробити передане представлення за власною семантикою; PUT створює або повністю замінює стан ресурсу; DELETE просить розірвати звʼязок ресурсу з його поточною функціональністю (це не обіцянка фізичного видалення рядка з бази). PATCH вносить часткові зміни, і різниця з PUT — у тому, чим є тіло: у PUT це модифікована версія ресурсу, у PATCH — набір інструкцій, як його змінити. Є ще HEAD — ідентичний GET, але сервер MUST NOT надсилати тіло, — і OPTIONS, який запитує опції взаємодії без дії над ресурсом; у браузері це насамперед CORS-preflight (див. «CORS і політика одного походження»).
Дві наскрізні властивості методів прямо годують надійність тестів.
Безпечний (safe) — той, чия визначена семантика фактично «лише читання»: клієнт не просить і не очікує зміни стану на сервері. Безпечні — GET, HEAD, OPTIONS, TRACE. Це не про захист даних і не про повну відсутність побічних ефектів: сервер може писати в лог доступу, важливо лише, що клієнт цього не просив і не відповідає за це. Мета поділу названа в специфікації прямо — щоб й працювали «without fear of causing harm».
Ідемпотентний (idempotent) — той, у якого намірений ефект кількох однакових запитів дорівнює ефекту одного. Такими є PUT, DELETE і всі ; неідемпотентні — POST, PATCH і CONNECT. Ключова тонкість: стосується стану сервера, а не тіла відповіді. DELETE /users/42 двічі поспіль лишає систему в тому самому стані, хоч перший виклик поверне 204, а другий — 404. Для PATCH є пряма норма: «PATCH is neither safe nor idempotent», — але конкретний запит можна зробити ідемпотентним, і специфікація радить умовний запит (наприклад, зі строгим ETag в If-Match), щоб два одночасні патчі не затерли один одного.
| Операція | Метод | Безпечний | Ідемпотентний |
|---|---|---|---|
| Читання | GET | так | так |
| Створення | POST | ні | ні |
| Повна заміна | PUT | ні | так |
| Часткова зміна | PATCH | ні | ні |
| Видалення | DELETE | ні | так |
| Метадані без тіла | HEAD | так | так |
| Опції взаємодії | OPTIONS | так | так |
Практичний наслідок для тестів прямий: ідемпотентний запит можна безпечно повторити після чи моргання мережі, а наївний повтор POST створить другий обʼєкт — звідси «привиди» в базі, які потім валять сусідні тести. Кешованість — окрема вісь, яка з цими двома не збігається: за замовчуванням кешовані лише GET і HEAD. І останнє: HTTP описує наміри, а не гарантує поведінку реалізації. GET, який щось мутує, технічно можливий — і саме тому це баг контракту, а не особливість. Канонічний розбір методів і заголовків — у главі «HTTP: методи, структура, заголовки».
Статус-код — частина контракту, а не «щось повернулося»
Статус-код — тризначне число в діапазоні 100–599; клас задає перша цифра, дві останні класифікаційної ролі не мають. поруч із кодом специфікація називає необовʼязковою, а клієнту приписує (SHOULD) її ігнорувати — тому в тесті асертять число, а не рядок.
| Код | Що каже про контракт |
|---|---|
200 OK | успіх; конкретний зміст залежить від методу |
201 Created | створено новий ресурс; за домовленістю з Location на його адресу |
204 No Content | тіла немає, заголовки значущі; парсити нічого |
400 Bad Request | помилка клієнта, зокрема зламаний синтаксис |
401 Unauthorized | не автентифіковано; відповідь мусить нести WWW-Authenticate |
403 Forbidden | клієнт серверу відомий, але прав на ресурс немає |
404 Not Found | і невідомий шлях, і відсутній обʼєкт за валідним ендпоінтом |
405 Method Not Allowed | метод відомий, але не підтримується цим ресурсом |
409 Conflict | запит конфліктує з поточним станом |
415 Unsupported Media Type | формат тіла не підтримується для цього ресурсу |
422 Unprocessable Content | форма коректна, семантика — ні |
429 Too Many Requests | перевищено рейт-ліміт; може нести Retry-After |
5xx | сервер не впорався з очевидно валідним запитом |
Дві пари кодів дають половину негативних сценаріїв. 400 проти 422: «я не зрозумів, що ти написав» проти «я зрозумів, але так не можна». 401 проти 403: «не автентифіковано» проти «автентифіковано, але не дозволено». Окремо варто знати, що 429 визначає не основна специфікація HTTP, а окремий документ — RFC 6585, і що Retry-After несе або число секунд, або HTTP-дату. І ще: на відверто кривий ввід добре спроєктований сервер відповідає 400 чи 422, а не падає з 500, — тож 5xx майже завжди означає щонайменше одну проблему на бекенді. Повна таблиця з розборами — у главі «HTTP статус-коди».
Наша практика (не канон). Реальний антипатерн, якого специфікації не описують: 200 OK із тілом {"error": "not found"}. Тест, що дивиться лише на статус, від такого зеленіє. Мінімальний чесний асерт складається з трьох частин — статус, Content-Type і структура тіла; повний чек-лист перевірок відповіді живе в розділі про API-тестування.
Тіло повідомлення й типи вмісту
Тіло (body) — необовʼязкова частина повідомлення після порожнього рядка. У запиті воно несе дані на сервер, у відповіді — запитані дані або пояснення . Що саме лежить у тілі, оголошує Content-Type, а скільки його — Content-Length; без них не знає ні формату, ні межі даних. Тіло в запиті за MDN мають лише PATCH, POST і PUT; у відповіді тіло є в більшості випадків, але не завжди — 204 No Content не несе його за означенням, тож парсити там нічого. Тіло в GET синтаксис не забороняє, але воно не має загальновизначеного значення й не може змінити зміст чи ціль запиту — тому складні «читальні» запити роблять через POST.
Три формати тіла, які QA бачить щодня:
application/json— вкладені обʼєкти й масиви з типами; формат сучасних REST-API;application/x-www-form-urlencoded— плоскі париключ=значення, зʼєднані&, зі спецсимволами у відсотковому кодуванні (пробіл —+); це те, що історично шле HTML-форма;multipart/form-data— тіло, розбите на частини з власними міні-заголовками; частини розділяє рядок із двох дефісів плюс значенняboundaryзContent-Type, а завершальний роздільник має ще два дефіси в кінці. Значенняboundaryне має права трапитися всередині вмісту.
Класична причина «у браузері працює, а в тесті — ні» саме тут: Content-Type не відповідає реальному тілу. Канонічна відповідь на це — 415 Unsupported Media Type, а не 400: специфікація прямо називає причиною «the request's indicated Content-Type or Content-Encoding». Сервер, що відповідає 400, дає менш точний код: 400 описує загальну помилку клієнта, а не відмову саме через медіа-тип, — і в баг-репорті це варто називати. Окремо про multipart: складати його руками легко зіпсувати на boundary чи заголовках частин, тому файл передають обʼєктом і дають бібліотеці зібрати тіло самій.
// Playwright: тіло й Content-Type формує клієнт, а не ви руками
await request.post('/api/v1/users', {
data: { name: 'Alice', roles: ['admin', 'qa'] }, // → application/json
});
await request.post('/api/v1/avatars', {
multipart: { file: { name: 'a.png', mimeType: 'image/png', buffer } },
});
Коли тіло не текст: gRPC і protobuf
Не кожен HTTP-обмін узагалі призначений для читання очима. gRPC за власною специфікацією переноситься поверх кадрування HTTP/2, кожен виклик на дроті — це POST, Content-Type починається з application/grpc (інакше сервер SHOULD відповісти 415), а результат їде не в статус-коді: і успіх, і помилка приходять зі :status 200, тоді як справжній код лежить у трейлері grpc-status. Дані серіалізує Protocol Buffers — мовно- і платформо-нейтральний механізм серіалізації структурованих даних, який у gRPC стоїть дефолтом, а не є його властивістю. Через це тіло gRPC-виклику в мережевій панелі не читається як JSON, а побайтове порівняння двох повідомлень некоректне за побудовою: ті самі дані мають багато різних бінарних представлень. Розгорнутий розбір gRPC і protobuf — у главі «SOAP, gRPC і асинхронні API».
JSON: шість типів і те, чого в ньому немає
JSON (JavaScript Object Notation) — легкий текстовий формат обміну, незалежний від мови програмування. Типів значень рівно шість: чотири прості — рядок, число, булеве значення, null — і два структурні: обʼєкт і масив. Медіа-тип формату — application/json, і саме він стоїть у Content-Type. JSON, яким обмінюються системи поза замкнутою екосистемою, МАЄ бути закодований у UTF-8.
{
"id": 42,
"name": "Alice",
"roles": ["admin", "qa"],
"verified": true,
"phone": null,
"profile": { "city": "Kyiv" }
}
Половина пасток формату — не в тому, що в ньому є, а в тому, чого немає.
- Обʼєкт невпорядкований, масив упорядкований. Це сказано в специфікації прямо, тож порядок ключів не гарантований, а порядок елементів масиву — значущий. Асерт, що спирається на послідовність ключів, ненадійний за побудовою.
- Унікальність імен в обʼєкті — рекомендація, а не вимога: «The names within an object SHOULD be unique». Документ із дублями лишається валідним, а поведінка приймача, за тим самим абзацом, «unpredictable», і багато реалізацій беруть останню пару. Отже, дублікати ключів ловить тест, а не парсер.
- Тип числа один. Окремих
intіfloatнемає, провідні нулі заборонені. Не покладайтеся на те, що поле прийде саме цілим, — це залежить від серіалізації на бекенді. - Типу «дата» немає. Дати передають рядком (найчастіше за форматом RFC 3339 / ISO 8601) або числом, і конкретний формат — частина контракту, яку перевіряють явно.
- Коментарів немає. Коментар у файлі, що претендує на JSON, робить його невалідним.
null— це значення, а не відсутність поля.{"phone": null}і{}— різні контракти, і плутати їх у тестах дорого.- Рядок береться лише в подвійні лапки, а самі лапки, зворотну скісну риску й керівні символи треба екранувати; літерали
true,false,nullпишуться лише малими.
Значенням у JSON може бути обʼєкт або масив, тому реальні відповіді вкладені на кілька рівнів: у прикладі вище roles — масив рядків, profile — вкладений обʼєкт, а шлях до міста виглядає як profile.city. Уміння впевнено читати такі шляхи — щоденний навик при написанні асертів.
Як читати JSON у тесті й не наступити на серіалізацію
Вхідний напрямок простий: Content-Type оголошує, що лежить у тілі, а парсинг JSON — окремий крок, який на невалідному вході падає. Порожнє тіло, HTML-сторінка помилки від замість JSON, обірваний потік — усе це для JSON.parse однаково невалідний вхід, і виняток на ньому маскує справжню причину: ви шукатимете баг у форматі даних замість 502, який стоїть у відповіді.
Вихідний напрямок — те, що ваш код кладе в тіло, — тонший. Date у JSON не «магічний»: у нього є toJSON(), який повертає те саме, що toISOString(), і JSON.stringify() серіалізує саме результат toJSON() — тобто на сервер їде рядок, і зворотний JSON.parse дати не відновить. Ще тонше з undefined: такі значення не є валідними для JSON, тож в обʼєкті поле просто зникає, а в масиві перетворюється на null, і довжина масиву не змінюється. І окремо: JSON.stringify() може повернути не рядок, а undefined, — тоді тіло запиту виявиться порожнім, а сервер відповість 400, і винен буде не він.
Наша практика (не канон). Порядок дій у тесті специфікації не описують — це наша конструкція, зібрана з двох фактів вище: спершу статус і Content-Type, і лише потім парсинг тіла.
const res = await request.get('/api/v1/users/42');
expect(res.status()).toBe(200);
expect(res.headers()['content-type']).toContain('application/json');
const user = await res.json(); // парсимо лише після двох перевірок вище
expect(user.roles).toContain('admin');
Типові помилки
- Виглядає як «REST — це JSON поверх HTTP», а насправді REST — набір із шести обмежень, у яких формат даних не згадано жодного разу. API може віддавати JSON і не бути RESTful, і навпаки.
- Виглядає як «прийшло 200, отже все добре», а насправді статус описує обробку на рівні протоколу, а не правильність даних:
200із тілом-помилкою — реальний антипатерн, від якого тест хибно зеленіє. - Виглядає як «
DELETEдвічі повернув404— метод не ідемпотентний», а насправді ідемпотентність — про стан сервера, а не про код відповіді. Ресурсу немає і після першого, і після другого виклику; різні коди це не порушують. - Виглядає як «
PATCHже оновлення, отже ідемпотентний», а насправді специфікація каже прямо:PATCHані безпечний, ані ідемпотентний. Ідемпотентним можна зробити конкретний запит — умовним, зIf-Match. - Виглядає як «сервер зламався, бо
400», а насправді тіло не відповідає оголошеномуContent-Type, і канонічна відповідь на це —415. Спершу звірте заголовок із реальним тілом, потім заводьте баг. - Виглядає як «поле прийшло порожнім», а насправді
nullі відсутнє поле — різні контракти. Асерт «поле є» і асерт «поле неnull» перевіряють різні речі. - Виглядає як «пробіл — це
%20», а насправді уapplication/x-www-form-urlencodedпробіл кодується+, а плюс —%2B. Той самий рядок, зібраний двома шляхами, не збігається побайтово, хоч логічно однаковий. - Виглядає як «відповідь змінилася — регресія», а насправді порядок ключів в обʼєкті ніколи не був гарантований, а нове поле у відповіді — сумісна зміна. Ламається тут не API, а асерт на рівність усього тіла.
Підсумок
- Ресурс — це «що», ендпоінт — «де», метод — «що зробити», представлення — «в якому вигляді». Чотири різні речі, які легко злипаються в одне слово «API».
- REST задає обмеження, а не формат. Найважливіше з них для тестів — : кожен запит несе все потрібне сам, тому його можна повторити ізольовано, а стан для сценарію підготувати одним викликом.
- Контракт відповіді складається щонайменше з трьох частин — статус,
Content-Typeі структура тіла. Перевірка однієї частини з трьох дає хибно-зелений результат. - Безпечність, ідемпотентність і кешованість — три різні осі. Ідемпотентність описує стан сервера після повтору, а не однаковість відповідей, і саме вона відповідає на питання «чи безпечно тут ретраїти».
- JSON бідний навмисно. Немає дат, коментарів і поділу чисел; порядок ключів не гарантований,
nullне дорівнює відсутньому полю. Усе, чого формат не дає, домовляються поверх нього — і саме ці домовленості перевіряє тест.
Далі ця грамотність стає інструментом: розділ про API-тестування бере ту саму модель і вчить будувати на ній перевірки — чек-лист відповіді, схеми й контракти, Postman і Newman, OpenAPI як джерело істини, тест-дизайн для CRUD і негативні сценарії. Тут була платформа, там буде метод.
Можливі питання
- «Що таке REST і чи є він стандартом?» Інтервʼюер перевіряє, чи не зводиться відповідь до «це JSON по HTTP». Сильна відповідь називає стиль, автора й хоча б чотири обмеження з шести — і окремо зазначає, що формат даних до стилю не належить.
- «Чим ендпоінт відрізняється від ресурсу?» Питання на точність мислення. Очікують розведення «концептуальне відображення» проти «конкретна адреса» і висновок: адреса стабільна, вміст за нею змінюється.
- «Які методи безпечні, а які ідемпотентні?
POSTідемпотентний?» Дивляться, чи знаєте ви обидва переліки й чи не плутаєте властивості. Плюс за згадку, що всі безпечні методи ідемпотентні, але не навпаки. - «
DELETEповторили — прийшов404. Це порушення ідемпотентності?» Класична пастка. Правильна відповідь розводить стан сервера й код відповіді. - «У чому різниця між
401і403, між400і422?» Питання про негативні сценарії. Очікують не переклад назв, а два формулювання: хто ти проти що тобі можна; не зрозумів проти зрозумів, але так не можна. - «Що буде, якщо надіслати JSON-тіло з
Content-Type: application/x-www-form-urlencoded?» Перевіряють, чи розумієте ви роль заголовка. Сильна відповідь називає415як канонічний код і згадує, що на практиці трапляється й загальніший400. - «Які типи значень є в JSON і чого в ньому немає?» Базове питання з подвійним дном: перелічити шість типів мало, цінується друга половина — немає дат, коментарів, поділу чисел, а
nullє значенням. - «Ви бачите
?q=search+results. Це пробіл чи плюс?» Дивляться на розуміння кодування: відповідь залежить від того, чи цеapplication/x-www-form-urlencoded; у шляху той самий символ означає літеральний плюс.
Джерела
Клієнт-сервер: рамка, у якій усе це живе
- RFC 9110 — HTTP Semantics — означення клієнта й сервера, проміжні
1xxперед єдиною фінальною відповіддю, незалежність семантики від версії протоколу. - Roy T. Fielding — Architectural Styles and the Design of Network-based Software Architectures, ch.5 (REST) — розділення відповідальностей у client-server і обмеження layered system.
- MDN — HTTP messages — склад запиту й відповіді.
REST — це набір обмежень, а не формат даних
- Roy T. Fielding — Architectural Styles and the Design of Network-based Software Architectures, ch.5 (REST) — REST як архітектурний стиль і всі шість обмежень, зокрема чотири підобмеження єдиного інтерфейсу й code-on-demand як єдине необовʼязкове.
Безстановість: кожен запит самодостатній
- RFC 9110 — HTTP Semantics — HTTP як безстановий протокол, заборона припускати звʼязок двох запитів на одному зʼєднанні, мотив масштабованості.
- Roy T. Fielding — Architectural Styles and the Design of Network-based Software Architectures, ch.5 (REST) — обмеження stateless і наслідок для видимості запиту.
- RFC 6265 — HTTP State Management Mechanism —
Set-Cookieі повернення кукі агентом користувача. - RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage — форма запиту з Bearer-токеном.
- Playwright — API testing — логін через API-виклик із наступним перенесенням стану в .
Ресурс і ендпоінт: чому це не синоніми
- Roy T. Fielding — Architectural Styles and the Design of Network-based Software Architectures, ch.5 (REST) — ресурс як усе, чому можна дати назву, ресурс як концептуальне відображення, ідентифікатор ресурсу й представлення.
- RFC 3986 — URI Generic Syntax — шлях і запит як компоненти, що разом ідентифікують ресурс.
- RFC 9110 — HTTP Semantics — дію над ресурсом задає метод.
- Martin Fowler — Tolerant Reader — читач має бути терпимим до додавання полів у відповідь.
Адреса: що з URL доходить до сервера
- RFC 3986 — URI Generic Syntax — граматика URI, які компоненти клієнт надсилає серверу, відокремлення фрагмента, нормалізація регістру схеми й хоста, відсутність вимог до структури рядка запиту.
- WHATWG URL Standard — типові порти особливих схем, які опускають у серіалізації.
- RFC 9110 — HTTP Semantics — рекомендована нижня межа підтримки довжини URI у 8000 октетів.
- Apache HTTP Server 2.4 — Core Features (mod_core) — дефолти
LimitRequestLineіLimitRequestBodyяк практичні межі адреси й тіла. - nginx — ngx_http_core_module —
414на стартовий рядок, що не влазить у буфер.
Відсоткове кодування в шляху й рядку запиту
- RFC 3986 — URI Generic Syntax — механізм відсоткового кодування, незарезервовані символи, UTF-8 як проміжний крок, регістронезалежність шістнадцяткових цифр.
- WHATWG URL Standard — набори символів залежно від компонента й пробіл як
+уapplication/x-www-form-urlencoded. - MDN — encodeURIComponent() — поведінка функції для пробілу й
&. - MDN — encodeURI() — перелік роздільників, які функція не кодує, і правило вибору між двома функціями.
Метод — це операція, а не частина адреси
- RFC 9110 — HTTP Semantics — семантика
GET,POST,PUT,DELETE,HEAD,OPTIONS; означення безпечного й з переліками; мета поділу; ідемпотентність як властивість стану сервера. - MDN — HTTP request methods —
PATCHяк часткова зміна; зведена таблиця безпечності, ідемпотентності й кешованості: звідсиCONNECTсеред неідемпотентних і кешованість за замовчуванням лише дляGETіHEAD. - RFC 5789 — PATCH Method for HTTP —
PATCHані безпечний, ані ідемпотентний; умовний запит як спосіб зробити конкретний патч ідемпотентним; різниця тілPUTіPATCH. - MDN — Idempotent (Glossary) — ідемпотентність як однаковий намірений ефект на сервері й приклад із повторним
DELETE, у якого коди відповідей різні. - MDN — Cross-Origin Resource Sharing (CORS) —
OPTIONSяк preflight-запит браузера.
Статус-код — частина контракту, а не «щось повернулося»
- RFC 9110 — HTTP Semantics — статус-код як тризначне число 100–599, класи за першою цифрою, необовʼязковість пояснювальної фрази, вимога
WWW-Authenticateпри401,415як відмова через формат тіла, межа між4xxі5xx. - MDN — HTTP response status codes — значення
200,201зLocation,204без тіла,400проти422,401проти403,404,405,409,429зRetry-After. - RFC 9112 — HTTP/1.1 (message syntax) — вимога до клієнта ігнорувати пояснювальну фразу статусу.
- RFC 6585 — Additional HTTP Status Codes — документ, який вводить
429.
Тіло повідомлення й типи вмісту
- MDN — HTTP messages — тіло як частина повідомлення після заголовків; які методи запиту мають тіло; тіло присутнє в більшості відповідей.
- RFC 9110 — HTTP Semantics —
Content-TypeіContent-Length, тіло вGETбез визначеної семантики,415як відповідь на непідтримуваний медіа-тип. - RFC 7578 — Returning Values from Forms: multipart/form-data — будова
multipart/form-data: частини з міні-заголовками,boundaryіfilename. - RFC 2046 — MIME Part Two: Media Types — роздільник як два дефіси плюс значення
boundary, завершальний роздільник, заборона збігу з вмістом. - WHATWG URL Standard —
application/x-www-form-urlencodedяк список пар «імʼя-значення» з пробілом у вигляді+. - Playwright — class APIRequestContext — передавання файлу обʼєктом замість ручного складання
multipart.
Коли тіло не текст: gRPC і protobuf
- gRPC over HTTP2 — протокольна специфікація gRPC — gRPC поверх кадрування HTTP/2, виклик як
POST,:status 200для успіху й помилки,grpc-statusу трейлері,415на чужийContent-Type. - gRPC — Core concepts, architecture and lifecycle — protobuf як мова опису інтерфейсу за замовчуванням, яку можна замінити.
- Protocol Buffers — Overview — механізм серіалізації структурованих даних і множинність бінарних представлень тих самих даних.
JSON: шість типів і те, чого в ньому немає
- RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format — чотири прості й два структурні типи, невпорядкований обʼєкт проти впорядкованого масиву,
SHOULDна унікальність імен і непередбачувана поведінка при дублях, запис чисел і рядків, відсутність типу «дата»,nullяк значення, UTF-8 і медіа-типapplication/json. - RFC 3339 — Date and Time on the Internet: Timestamps — формат дати-часу, яким передають дати всередині JSON-рядка.
Як читати JSON у тесті й не наступити на серіалізацію
- MDN — JSON (JavaScript reference) — парсинг тексту як окремий крок, що падає на невалідному вході.
- RFC 9110 — HTTP Semantics —
Content-Typeяк оголошення формату тіла, на яке спирається рішення парсити. - MDN — JSON.stringify() — серіалізація через
toJSON(), доляundefinedв обʼєкті й масиві, поверненняundefinedзамість рядка.
Що таке REST і чи є він стандартом?
REST — архітектурний стиль, а не протокол і не специфікація, яку можна «підтримувати галочкою». Його сформулював Рой Філдінг (Roy Fielding) у дисертації 2000 року як набір обмежень на взаємодію компонентів розподіленої гіпермедійної системи; система, що їх виконує, зветься RESTful. Обмежень шість: client-server, stateless, cache, uniform interface, layered system і code on demand — останнє єдине необовʼязкове. Найцінніша частина відповіді йде далі за перелік: у жодному з обмежень не сказано ані слова про формат даних. Тому API може віддавати ідеальний JSON і не бути RESTful, а RESTful-система має право обмінюватися чим завгодно. На практиці HATEOAS реалізують рідко, тож обережніше називати такі системи HTTP-API у стилі REST і не сперечатися про чистоту.
Чим ресурс відрізняється від ендпоінта?
Ресурс — це «що», — «де». Ресурсом у REST може бути будь-що, чому дають імʼя: документ, картинка, послуга, колекція, навіть жива людина. Важлива тонкість означення: ресурс — це концептуальне відображення на набір сутностей, а не конкретна сутність у конкретну секунду. Ендпоінт — просто адреса, за якою до цього ресурсу стукають. Третє поняття, яке зазвичай злипається з двома попередніми, — представлення (representation): байти плюс метадані, що описують поточний або бажаний стан ресурсу. Звідси проста перевірка розуміння: JSON у тілі відповіді — це представлення, а не ресурс, і за тією самою адресою завтра прийдуть інші байти, хоча ресурс лишиться тим самим.
Із чого складається запит і відповідь і чи завжди на запит приходить рівно одна відповідь?
Запит — це метод, шлях, заголовки й необовʼязкове тіло; відповідь — , заголовки й необовʼязкове тіло. Формула «на запит — відповідь» справджується строго лише для фінальної відповіді: перед нею сервер має право надіслати нуль або більше проміжних повідомлень класу 1xx, а завершальна після них буде рівно одна. Ще одна корисна властивість: семантика повідомлення не залежить від версії протоколу, тож може ретранслювати запит між HTTP/1.1 і HTTP/2, не змінюючи його змісту. Для QA з цього випливає практичний висновок: вердиктом про обробку є статус саме фінальної відповіді, а не проміжних 1xx. А ще памʼятайте про обмеження layered system: між вами й застосунком цілком легально стоять , балансувальники та CDN, тож відповідь інколи формує зовсім не той вузол, який ви тестуєте.
Що означає stateless і як тоді взагалі існує сесія користувача?
означає, що кожен запит несе все потрібне для свого розуміння, а сервер не має права спиратися на памʼять про попередні звернення — навіть два запити в одному зʼєднанні він не зобовʼязаний вважати запитами того самого агента. Мотив суто інженерний: якщо сервер нічого не памʼятає, будь-який вузол із десятка однакових обробить будь-який запит, а система моніторингу зрозуміє природу запиту з нього самого. «Памʼять» тому будують поверх протоколу двома способами: кукі, яку сервер видає заголовком Set-Cookie і яку агент користувача сам повертає в Cookie, і токен, який їде в заголовку Authorization у формі Bearer. Різниця для тесту принципова: кукі доїде автоматично, а токен покладе тільки той код, який ви написали. Найдешевший практичний наслідок: сесію для сценарію отримують одним API-викликом і підкладають у контекст, замість проклацувати форму входу в кожному тесті.
Які частини URL доїжджають до сервера, а які ні?
Сервер отримує authority, шлях і рядок запиту; фрагмент після # відсікається ще на клієнті й у логах API не зʼявляється ніколи — його обробляє винятково клієнт. Друга частина відповіді про регістр: схема й хост нечутливі до нього й нормалізуються в нижній, а шлях і рядок запиту вважаються чутливими, якщо схема не сказала іншого. Саме тому склеєний із /api/Users/42 може дати 404 там, де /api/users/42 працює. Третя деталь: типовий порт особливої схеми в серіалізації опускають, тож адреси з :443 і без нього для https тотожні. І четверта: структуру самого рядка запиту стандарт не регламентує зовсім — пари ключ=значення через & є домовленістю, успадкованою від HTML-форм, тому поведінка при дублюванні параметра залежить від конкретного бекенда, а не від стандарту.
Що таке відсоткове кодування і чому одна українська літера дає дві трійки?
Це спосіб подати байт трьома символами: знак відсотка й дві шістнадцяткові цифри його значення. Працює воно у два кроки, і саме тут інтуїція підводить: спершу текст стає послідовністю байтів (у вебі це майже завжди UTF-8), і аж потім кожен байт окремо переписується як %XX. Тому пробіл дає одну трійку %20, а українська «і» в UTF-8 займає два байти й перетворюється на %D1%96. Кодування потрібне там, де символ не належить до дозволеного набору або де він працює роздільником; натомість латинські літери й цифри разом із чотирма символами — дефісом, крапкою, підкресленням і тильдою — належать до незарезервованих і лишаються як є. Дві дрібниці для сильної відповіді: шістнадцяткові цифри регістронезалежні, тож %2F і %2f — те саме, а набір символів, які підлягають кодуванню, залежить від того, у якому компоненті адреси вони стоять.
У логах видно ?q=search+results. Це пробіл чи плюс?
Залежить від угоди, у якій зібрано рядок. У форматі application/x-www-form-urlencoded пробіл записують плюсом, а сам плюс доводиться екранувати як %2B; у шляху ж цей символ означає буквальний плюс. Ті самі граблі живуть у коді тесту: encodeURIComponent віддасть для пробілу %20 і закодує амперсанд, URLSearchParams для того самого пробілу дасть +, а encodeURI роздільники адреси лишає недоторканими, бо створений для вже зібраного URL цілком. Окреме динамічне значення тому кодують саме через encodeURIComponent. Практичний висновок дешевий: будуйте на декодованих значеннях — інакше два записи того самого фільтра, ?q=search+results і ?q=search%20results, розійдуться в порівнянні, хоча дані за ними ідентичні.
Чим PUT відрізняється від PATCH?
Обидва змінюють ресурс, але різняться тим, чим є тіло запиту. У PUT тіло — це нова версія ресурсу цілком: сервер створює його або повністю замінює поточний стан переданим. У PATCH тіло — набір інструкцій, як саме змінити те, що вже лежить на сервері, тобто часткова зміна. Звідси й різні властивості: PUT ідемпотентний, бо десять однакових замін дають той самий кінцевий стан, а PATCH за специфікацією не є ані безпечним, ані ідемпотентним. Класична пастка тут — надіслати в PUT тільки одне змінене поле: формально це заміна всього ресурсу, і решта полів може легально обнулитися. Окремо варто знати, що конкретний PATCH можна зробити ідемпотентним умовним запитом — наприклад, зі строгим ETag в If-Match, — щоб два паралельні патчі не затерли один одного.
Які методи безпечні, а які ідемпотентні? POST ідемпотентний?
— той, чия семантика зводиться до читання: клієнт не просить змінювати стан на сервері. Такими є GET, HEAD, OPTIONS і TRACE. Ідемпотентний — той, для якого повторення того самого запиту дає рівно той самий намірений результат, що й одна спроба: це PUT, DELETE і всі безпечні методи. POST не є ані безпечним, ані ідемпотентним, як і PATCH та CONNECT. Два уточнення, за які дають бали: усі безпечні методи ідемпотентні, але не навпаки, а сама безпечність не обіцяє повної відсутності побічних ефектів — сервер має право дописати рядок у лог доступу, і це нічого не порушує, бо такого запису клієнт не замовляв. І третя, окрема вісь — кешованість: за замовчуванням кешованими вважаються тільки GET і HEAD, тож зі згаданими двома властивостями вона не збігається.
DELETE повторили — прийшов 404. Це порушення ідемпотентності?
Ні, і це класична пастка на плутанині двох речей. описує стан сервера після повторення запиту, а не однаковість тіл чи кодів відповіді. Після першого виклику обʼєкта немає, після другого — теж немає, стан ідентичний, тож властивість збережено; те, що перший раз повернувся 204, а другий 404, її не стосується. Практичний наслідок цінніший за саме визначення: ідемпотентний запит не страшно відправити ще раз після чи моргання мережі, тоді як сліпий POST додасть у базу другий обʼєкт. Такі «привиди» в базі потім валять сусідні тести, і шукати причину доводиться зовсім не там, де вона зʼявилася. Тому за собою в afterEach пишуть так, щоб 404 на другому проході вважався нормою.
У чому різниця між 400 і 422, 401 і 403?
Це дві пари, які закривають половину негативних сценаріїв, і обидві розводяться однією фразою. 400 проти 422: «я не зрозумів, що ти надіслав» проти «я все розібрав, але так не можна» — тобто зламаний синтаксис проти коректно зібраного запиту з неможливим змістом. 401 проти 403: «не знаю, хто ти» проти «знаю, але тобі не можна» — брак автентифікації проти в доступі вже впізнаному клієнту. Дві деталі підвищують оцінку: відповідь 401 мусить нести заголовок WWW-Authenticate, а повторний логін при 403 нічого не змінить, бо стадію впізнавання вже пройдено. І окремо памʼятайте межу класів: на сміттєвий ввід зріла реалізація має відповідати кодом класу 4xx, а 500 у відповідь на такий запит — самостійна знахідка, а не очікуваний результат негативного тесту.
Що буде, якщо надіслати JSON із заголовком Content-Type: application/x-www-form-urlencoded?
Канонічна відповідь сервера — 415 Unsupported Media Type: специфікація прямо називає причиною цього коду непідтримуваний Content-Type або Content-Encoding запиту. На практиці частина бекендів віддає загальний 400 — це менш точно: 400 описує будь-яку помилку клієнта, а не відмову саме через формат тіла. Сильна відповідь на співбесіді називає обидва варіанти й додає, що в баг-репорті різницю варто зафіксувати. Причина такої ситуації майже завжди одна — заголовок не збігається з тим, що реально поїхало в тілі, і це найпоширеніше джерело «у браузері працює, а в тесті ні». Тому перед заведенням дефекту звіряють оголошений тип із фактичним тілом запиту, а не з очікуваннями.
Які три формати тіла QA бачить щодня і навіщо в multipart потрібен boundary?
Перший — application/json: вкладені обʼєкти й масиви з типами, дефолт сучасних HTTP-API. Другий — application/x-www-form-urlencoded: плоскі пари, зʼєднані амперсандом, зі спецсимволами у відсотковому кодуванні й пробілом у вигляді плюса; історично це те, що шле HTML-форма. Третій — multipart/form-data: тіло, розрізане на частини, кожна зі своїми міні-заголовками; саме ним відправляють файли. boundary тут — оголошений у Content-Type рядок-роздільник: частини відокремлює два дефіси плюс його значення, а фінальний роздільник має ще два дефіси в кінці. Головна вимога — значення не повинно трапитися всередині самого вмісту, інакше тіло розпадеться не там, де треба. Практичний висновок: multipart не складають руками, а передають файл обʼєктом і дають клієнтській бібліотеці зібрати тіло й заголовки самій.
З чого складається чесний асерт на відповідь API і чому не порівнюють усе тіло цілком?
Мінімальний чесний асерт має три частини: статус-код, Content-Type і структура тіла. Перевірка лише статусу дає хибно-зелений результат — реальний антипатерн 200 OK з тілом на кшталт помилки «не знайдено» проходить такий тест без жодного зауваження. Перевірка лише тіла теж неповна: без Content-Type ви не знаєте, чи мали право його парсити. А от порівнювати відповідь на повну рівність із еталоном — крихкий підхід: у межах однієї версії API поля прийнято додавати, і безпечне розширення відповіді ламатиме такий асерт щоразу. Правильна стратегія — : перевіряти саме ту підмножину полів, від якої залежить сценарій. І окремо: гарантій на послідовність ключів в обʼєкті не було ніколи, тож будь-який асерт, що на неї спирається, ненадійний за побудовою.
Які типи значень є в JSON і чого в ньому немає?
Простих типів чотири — рядок, число, булеве значення й null; структурних два — обʼєкт і масив; разом шість, і нічого понад це формат не має. Медіа-тип — application/json, а для обміну поза замкнутою екосистемою документ має бути закодований у UTF-8. Друга половина відповіді цінується більше за першу: у JSON немає типу «дата», немає коментарів і немає поділу чисел на цілі й дробові — число одне, а провідні нулі заборонені. Дату тому везуть або рядком за RFC 3339 / ISO 8601, або числом, а домовленість про її форму стає частиною контракту й перевіряється окремим асертом. Рядок береться лише в подвійні лапки, лапки й зворотну скісну риску всередині екранують, а літерали true, false і null пишуться малими. Те, чого формат не має, команди домовляють окремо — і перевірка саме таких домовленостей лягає на тест.
Чи можна покладатися на порядок ключів і чи впаде парсер на дублікатах?
На порядок ключів — ні: специфікація прямо називає обʼєкт невпорядкованою колекцією, тоді як масив упорядкований. Тобто послідовність елементів масиву значуща й перевіряти її можна, а асерт на послідовність полів обʼєкта ненадійний за побудовою. З дублікатами ситуація ще підступніша: унікальність імен у JSON — рекомендація, а не вимога, тож документ із двома однаковими ключами лишається валідним. Поведінка в такому разі не визначена, а на практиці більшість реалізацій бере останню пару. Висновок для роботи прямий: дублікат ключа не підсвітить ані парсер, ані тип-чекер — його ловить лише тест, який дивиться на сирий текст відповіді або на схему.
Чим null відрізняється від відсутнього поля і чому це дорого коштує?
null у JSON — це повноцінне значення, одне з шести типів, а не спосіб сказати «поля немає». Тому обʼєкт із полем у значенні null і обʼєкт без цього поля описують два різні контракти: у першому випадку бекенд стверджує «значення відоме й воно порожнє», у другому — «даних про це немає взагалі». Асерт «поле присутнє» і асерт «поле не порожнє» перевіряють різні речі, і підміна одного одним дає або хибно-зелений тест, або нескінченні розслідування на порожньому місці. У коді тесту різниця теж має ціну: властивість зі значенням undefined при серіалізації просто зникне з обʼєкта, тобто ви надішлете не те, що думали. Тому в контракті на поле, яке може бути порожнім, завжди уточнюють, що саме приходить, — null чи відсутність ключа.
Що JSON.stringify() робить із Date і undefined?
Обидва випадки — типове джерело «я ж передав правильні дані». У Date є власний метод toJSON(), і його результат збігається з тим, що віддає toISOString(); у тіло запиту потрапляє саме цей рядок, тож зворотний розбір відповіді обʼєкт дати вже не відновить. Зі значеннями undefined поведінка асиметрична: в обʼєкті ключ із таким значенням до результату не потрапляє взагалі, а в масиві на його місці зʼявляється null, тож довжина масиву лишається колишньою. Є й третій, найнеприємніший варіант: серіалізація може повернути не рядок, а undefined, і тоді тіло запиту виявиться порожнім. Сервер на таке відповість 400, і на перший погляд винним виглядатиме він, хоча зламався клієнтський код. Тому при розслідуванні 400 спершу дивляться на фактично надіслане тіло в панелі Network, а не на те, що збирався надіслати код.
Тест падає на розборі JSON. З чого починати діагностику?
З перевірки того, чи взагалі там був JSON. Для парсера порожнє тіло, HTML-сторінка помилки від й обірваний потік — однаково невалідний вхід, і виняток у стектрейсі маскує справжню причину: ви шукатимете дефект у форматі даних замість того, щоб побачити 502 у статусі. Тому порядок дій у тесті зворотний до інтуїтивного: спершу статус, потім Content-Type, і лише після цього розбір тіла. Другий частий сценарій — відповідь, у якої тіла немає за контрактом: 204 No Content парсити нічого, і падіння тут означає помилку в тесті, а не в застосунку. Третій — проксі чи балансувальник, що повернув свою власну сторінку помилки з типом text/html; це видно за одним поглядом на заголовок відповіді.
Чому в gRPC статус 200 нічого не каже про успіх виклику?
Тому що gRPC використовує HTTP/2 як транспорт, а власний результат передає окремо. Кожен виклик на дроті — це POST із Content-Type, що починається з application/grpc; на чужий тип сервер має відповісти 415. І успіх, і помилка приходять зі :status 200, а справжній код операції лежить у трейлері grpc-status — тому звичний рефлекс «двісті, отже все добре» тут не працює. Друга особливість — дані серіалізує Protocol Buffers, платформо-нейтральний механізм, який у gRPC стоїть за замовчуванням, але не є його невідʼємною частиною. Практичні наслідки два: у мережевій панелі тіло такого виклику JSON-ом не прочитається, а порівнювати два повідомлення байт у байт безглуздо — одні й ті самі дані мають безліч валідних бінарних форм.
Три кейси, у яких модель «ресурс — метод — статус — тіло» перетворюється на конкретні дії: розбір скарги «кнопка не працює» за одним записом у панелі Network, контракту в Playwright, який не бреше і не сиплеться від сумісних змін, і рішення про після в CI.
Кейс 1. «Зберегти не працює»: розбір однієї відповіді за три шари
Скарга приходить у вигляді «нічого не зберігається». Замість здогадок беремо один запис у панелі Network і читаємо його трьома шарами: спершу статус (чия це проблема), потім Content-Type (чи маю я право парсити тіло), потім саме тіло (що конкретно не так). Три шари дають вердикт швидше, ніж будь-який лог.
| Що бачу у відповіді | Робоча гіпотеза | Перший крок перевірки |
|---|---|---|
422 з поясненням у тілі | бекенд запит зрозумів і забракував дані | читати тіло: яке поле й за яким правилом не пройшло |
415 | тіло не збігається з оголошеним Content-Type | звірити заголовок запиту з тим, що реально поїхало |
400 | сервер не розібрав запит | шукати биту серіалізацію: порожнє тіло, обірваний JSON |
401 | клієнт не автентифікований | сетап токена чи кукі; перевірити наявність WWW-Authenticate |
403 | впізнаний клієнт, але дії не дозволено | ролі й політика доступу, а не форма входу |
404 на валідному ендпоінті | немає обʼєкта — або промах адресою | регістр у шляху, префікс версії, чи те це середовище |
405 | адреса жива, метод не той | контракт ресурсу: які методи він узагалі підтримує |
200 з порожнім масивом | запит спрацював, даних під фільтр немає | параметри запиту й дані в середовищі, не верстка |
200 з тілом на кшталт {"error": "not found"} | антипатерн: помилка сховалася під успіх | дефект контракту, і асерт на самий лише статус тут сліпий |
429 | спрацював рейт-ліміт | скільки паралельних воркерів бʼють у цей ендпоінт |
5xx | бекенд не впорався з валідним запитом | логи застосунку; це знахідка навіть на кривому вводі |
Що тут найчастіше плутають:
415виглядає як400, а означає інше. Загальний400каже «я не розібрав запит», тоді як415називає причину точно: непідтримуванийContent-TypeабоContent-Encoding. Побачили415— не поспішайте до бекенду, спершу гляньте, чим ваш клієнт підписав тіло.404на живому — не завжди «немає обʼєкта». Регістр у схемі й хості значення не має, а от у шляху й рядку запиту має, тож/api/v1/Users/42законно промахнеться там, де працює/api/v1/users/42. Відрізнити одне від одного допомагає той самий шлях іншим методом:405означає, що ресурс за адресою є, просто цього методу він не підтримує.- Фрагмента адреси в логах API не буде ніколи. Усе після
#обробляє клієнт і на сервер не надсилає. Тому шукати причину «сервер не бачить мій параметр» у фрагменті марно — параметри живуть у рядку запиту. - Відповідь міг сформувати не застосунок. Між клієнтом і бекендом легально стоять , балансувальники й CDN, і сторінка помилки з типом
text/htmlзамість очікуваного JSON — типовий підпис такого .
Кейс 2. Playwright: асерт, який не бреше і не сиплеться
Дві крайності однаково погані. Асерт лише на статус пропускає 200 із тілом-помилкою; асерт на повну рівність тіла червоніє щоразу, коли бекенд додає нове поле — а додавання полів у межах версії вважається сумісною зміною. Робоча середина — три шари з першого кейсу плюс перевірка тієї підмножини даних, від якої залежить сценарій.
import { test, expect } from '@playwright/test';
test('GET /api/v1/users/42 віддає контракт користувача', async ({ request }) => {
const res = await request.get('/api/v1/users/42');
// 1. статус: вердикт про обробку
expect(res.status()).toBe(200);
// 2. тип вмісту: підстава взагалі парсити тіло
expect(res.headers()['content-type']).toContain('application/json');
// 3. структура: лише те, від чого залежить сценарій
const user = await res.json();
expect(user).toMatchObject({ id: 42, name: 'Alice' });
expect(user.roles).toContain('qa');
// null і відсутність ключа — різні контракти, тому перевіряємо саме те, що обіцяно
expect(user).toHaveProperty('phone');
expect(user.phone).toBeNull();
// дати в JSON немає: приходить рядок, і його формат — частина контракту
expect(typeof user.createdAt).toBe('string');
});
Створення ресурсу перевіряють тим самим шаблоном, але тіло формує клієнт — і саме тут ламаються заголовки.
test('POST /api/v1/orders створює замовлення', async ({ request }) => {
// data → application/json проставить сам клієнт; руками заголовок не чіпаємо
const res = await request.post('/api/v1/orders', {
data: { sku: 'A-100', qty: 2 },
});
expect(res.status()).toBe(201);
expect(res.headers()['location']).toBeTruthy();
});
Що дивитися і чому:
- Порядок кроків не косметичний. Розбір тіла — окрема операція, яка падає на будь-якому невалідному вході: порожня відповідь, HTML від , обірваний потік. Виняток у парсері маскує справжню причину, і ви шукаєте баг у форматі даних замість того, щоб побачити
502у статусі. 204не парсять взагалі. Якщо ендпоінт за контрактом віддає успіх без тіла, спроба дістати з нього JSON перетворить зелений сценарій на фальшиве падіння.- Порядок ключів перевіряти не можна, порядок елементів масиву — можна. Обʼєкт у JSON невпорядкований за специфікацією, масив упорядкований; асерт на послідовність полів ненадійний за побудовою.
- Дублікат ключа у відповіді парсер не підсвітить. Унікальність імен у JSON — рекомендація, документ із дублями лишається валідним, а більшість реалізацій мовчки бере останню пару. Хочете це ловити — дивіться на сирий текст відповіді або на схему.
- Порівнюйте декодовані значення параметрів, а не сирі рядки.
?q=search+resultsі?q=search%20resultsнесуть однакові дані, але побайтово не збігаються: у форміapplication/x-www-form-urlencodedпробіл записують плюсом, тоді якencodeURIComponentвіддає%20. Dateу тілі запиту стає рядком. Серіалізація викликаєtoJSON(), тож на сервер їде текст у форматі ISO, і зворотний розбір відповіді обʼєкт дати не відновить. А поле зі значеннямundefinedпри серіалізації просто зникне з обʼєкта — і сервер отримає не те, що ви збиралися надіслати.
Кейс 3. Таймаут у CI: що можна повторити, а що лишить привида
Мережа моргнула, клієнт не дочекався відповіді, тест упав. Спокуса очевидна — обгорнути все ретраєм. Але повторний запит безпечний рівно там, де метод ідемпотентний: скільки б разів ви не відправили той самий запит, намірений результат буде такий, як після однієї спроби. описує стан сервера, а не однаковість відповідей — це те місце, де плутанина коштує найдорожче.
| Крок сценарію | Метод | Чи можна повторити наосліп |
|---|---|---|
| Прочитати картку товару | GET | так: метод безпечний — клієнт не замовляє зміни стану |
| Створити замовлення | POST | ні: другий виклик створить другий обʼєкт |
| Замінити профіль цілком | PUT | так: кінцевий стан той самий після будь-якої кількості спроб |
| Змінити одне поле | PATCH | ні за замовчуванням; ідемпотентним конкретний запит робить умовний If-Match |
| Прибрати за собою | DELETE | так: обʼєкта немає ні після першого, ні після другого виклику |
Найбільша шкода від наївного повтору POST не в самому дублікаті, а в тому, де він вистрелить: другий обʼєкт спокійно доживе до наступного тесту й зламає його асерт на кількість записів. Причину такого падіння шукають у зовсім іншому файлі, ніж та, що її створила.
пишуть із урахуванням того, що DELETE ідемпотентний, а коди двох викликів різні:
test.afterEach(async ({ request }) => {
const res = await request.delete(`/api/v1/orders/${orderId}`);
// 404 на другому проході — норма: стан сервера вже потрібний, обʼєкта немає
expect([204, 404]).toContain(res.status());
});
Що дивитися і чому:
- Ідемпотентність — не про тіло відповіді. Перший
DELETEвіддасть204, повторний —404, і це не порушення властивості: стан системи в обох випадках однаковий. Асерт, прибитий рівно до204, зробить прибиральник ламким. PATCHне рятує «бо це ж оновлення». Специфікація прямо каже, що він ані безпечний, ані ідемпотентний. Якщо повтор потрібен, конкретний запит роблять умовним — зі строгимETagвIf-Match, — і два паралельні патчі перестають затирати один одного.- Кешованість — окрема вісь. Те, що метод безпечний, не робить його відповідь кешованою автоматично: за замовчуванням кешованими вважаються лише
GETіHEAD. Плутати ці властивості — типова помилка на співбесіді. GET, який щось змінює, — дефект контракту. Протокол описує наміри, а не гарантує поведінку реалізації, тож технічно такий ендпоінт можливий. Знайшли — це баг, а не «особливість нашого API», і ретраї на ньому небезпечні попри «безпечний» метод.
REST як стиль, а не формат
- Можу назвати автора, рік і природу REST: архітектурний стиль, а не протокол чи стандарт із сертифікацією відповідності.
- Памʼятаю всі шість обмежень і те, яке з них єдине необовʼязкове (code on demand).
- Розумію, чому «REST — це JSON поверх HTTP» хибна формула: формат серіалізації до стилю не належить узагалі.
Безстановість і стан у тесті
- Можу пояснити stateless через самодостатність запиту й назвати мотив — масштабованість плюс видимість запиту для моніторингу.
- Знаю різницю кукі vs токен за способом доставки: одну повертає агент користувача сам, другий кладе лише код застосунку — тому сесію для сценарію дешевше готувати одним API-викликом.
Ресурс, ендпоінт, адреса
- Знаю різницю ресурс vs vs представлення: «що», «де» і «в якому вигляді» — і можу пояснити, чому вміст за адресою змінюється, а сам ресурс лишається тим самим.
- Памʼятаю, які частини URL їдуть на сервер, а що відсікається на клієнті (фрагмент), і де регістр значущий: шлях і рядок запиту, але не схема з хостом.
- Знаю різницю
%20vs+уapplication/x-www-form-urlencodedі розумію, чому одна українська літера дає дві трійки: текст спершу стає байтами UTF-8, і кожен байт кодується окремо.
Методи та їхні властивості
- Можу назвати обидва переліки напамʼять: безпечні —
GET,HEAD,OPTIONS,TRACE; ідемпотентні —PUT,DELETEплюс усі безпечні; кешованість за замовчуванням — третя, окрема вісь і лише дляGETтаHEAD. - Знаю різницю
PUTvsPATCHза вмістом тіла: нова версія ресурсу проти набору інструкцій зі зміни. - Можу відповісти, чому
DELETEіз404на другому виклику не порушує ідемпотентності, і знаю, які запити безпечно повторювати після , а де наївнийPOSTлишить у базі привида.
Статус-код і контракт відповіді
- Можу розвести дві пари одним рядком:
400vs422і401vs403. - Знаю, що канонічна відповідь на невідповідний
Content-Type—415, а не400, і можу це аргументувати в баг-репорті. - Тримаю порядок перевірки відповіді: статус,
Content-Type, і лише потім структура тіла — і памʼятаю, чим небезпечний200із тілом-помилкою.
Тіло, формати, JSON
- Знаю три щоденні формати тіла й те, чим
boundaryтримає купиmultipart/form-data. - Можу перелічити шість типів JSON і, що важливіше, назвати чого в ньому немає: дат, коментарів, поділу чисел на цілі й дробові.
- Знаю різницю
nullvs відсутнє поле: це два різні контракти, а не синоніми. - Памʼятаю, що обʼєкт невпорядкований, а масив упорядкований, і що дублікат ключа лишає документ валідним — ловить його тест, а не парсер.
- Розумію, чому на рівність усього тіла крихкий: додавання полів — сумісна зміна, тому перевіряю потрібну підмножину.
- Можу пояснити, що
JSON.stringify()робить ізDateіundefinedі чому через це тіло запиту інколи їде порожнім.
Квіз
Перед стартом
- Питань: 16
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Яке з шести обмежень REST єдине необовʼязкове?


