1. Алгоритм розбору HTTP-виклику
На цьому етапі вже видно чотири різні джерела болю: запит може бути несамодостатнім, операція може змінювати ресурс, відповіді може не бути, а отримана відповідь ще не зобов’язана збігатися з домовленістю. Тож корисно зібрати ці частини в один робочий порядок розбору, щоб не діагностувати HTTP на око і не виправляти не ту проблему. Інструмент згодом може сховати частину службового коду, але причини збою все одно розкладаються за тими самими питаннями.
Коли щось іде не так, мозок новачка зазвичай робить героїчний стрибок у середину історії: «О! У body якийсь дивний рядок», «О! Прилетів 404 — значить сервер упав», «О! Виняток — значить баг у бізнес-логіці». Проблема в тому, що це не аналіз, а ворожіння, тільки з красивішими термінами.
Backend-мислення любить порядок, тому що порядок економить час. Якщо ви щоразу розбираєте проблеми за однією схемою, ви швидше знаходите причину, менше сваритеся з провайдером (інколи даремно) і рідше «виправляєте не те». Це особливо важливо для ReadLater Starter: щойно застосунок звертається до зовнішнього каталогу книжок, доводиться розрізняти «сервіс відповів помилкою», «ми не отримали відповіді» та «відповідь прийшла, але це не те, що обіцяв контракт».
Якщо коротко: алгоритм потрібен не тому, що ви не розумієте, а тому, що ви жива людина. А живим людям корисні чек-листи, інакше вони починають дебажити за гороскопом.
Три питання: відповідь, статус, контракт
Корисно тримати в голові три питання, які потрібно ставити строго в порядку. Цей порядок важливіший, ніж здається: він захищає від найпоширенішої помилки — «спочатку полізли в body, а потім зʼясувалося, що відповіді взагалі не було». Схема проста, майже як «подивіться, чи комп’ютер увімкнено в розетку», тільки в HTTP.
Перше питання: відповідь взагалі була? Якщо відповіді не було, то ви не можете аналізувати статус-код за означенням — його немає. Друге питання: якщо відповідь була, який статус? Статус — це швидка мітка змісту: успіх, помилка запиту, відсутність ресурсу, конфлікт, внутрішня помилка. Третє питання: якщо статус успіху, чи відповідає відповідь контракту? Тому що 200 OK — це ще не гарантія, що вам надіслали саме те, про що домовлялися.
Ось це можна уявити як дуже просте дерево рішень:
flowchart TD
A["Проблемний HTTP-виклик"] --> B{Відповідь отримано?}
B -- Ні --> C["Мережевий збій / немає відповіді"]
B -- Так --> D{"Код статусу"}
D -- 4xx/5xx --> E["Отримано відповідь із помилкою"]
D -- 2xx --> F{Контракт виконано?}
F -- Ні --> G["Порушення контракту"]
F -- Так --> H["Успіх"]
Зверніть увагу на важливу психологічну річ: 404 перебуває в гілці «відповідь отримано». Це означає, що сервер живий, мережа доставила запит і повернула відповідь. Це вже корисна інформація, і вона кардинально відрізняється від ситуації «відповіді взагалі немає».
2. Крок 1 — перевірка наявності відповіді
Починати розбір потрібно з найнуднішого, але найрятівнішого питання: «Відповідь була?» Нудно — тому що хочеться одразу читати body і шукати, що там не так. Рятівно — тому що інколи жодної HTTP-відповіді не існує, і ви намагаєтеся аналізувати фантом.
Якщо відповіді немає, далі вже немає про що сперечатися на рівні статусів: у вас немає ні 404, ні 500, ні заголовків, ні тіла. Причини можуть бути різні — сервіс недоступний, адреса неправильна, мережа відвалилася, зʼєднання не встановилося, — але гілка діагностики одна: спочатку визнаємо, що response не отримано, і лише потім зʼясовуємо, чому.
Щоб зафіксувати думку на простому Java-прикладі, можна уявити, що результат виклику — це або response, або помилка. Зараз ми не пишемо реальний HTTP-код (він буде пізніше), але можемо змоделювати форму результату — це корисно для мислення.
package com.example.readlater;
// Результат виклику: або є HTTP-відповідь, або на шляху до неї сталася помилка (мережа, DNS, таймаут тощо)
public record CallOutcome(RemoteResponse response, Exception error) {
// Головне в цьому об’єкті: ми відокремлюємо "немає відповіді" від "відповідь із поганим статусом".
public boolean hasResponse() {
// Якщо response == null — це означає, що HTTP-відповіді не було як такої (нічого аналізувати за статусом/заголовками).
return response != null;
}
}
Якщо hasResponse() повернув false, не треба шукати «правильний статус» і тим більше розбирати body: у вас інша гілка проблеми. Тут зʼясовують, чи був мережевий збій, чи не промахнулися ви в host/port/path і чи не вперлися у зовнішню інфраструктуру.
І тут корисно пам’ятати семантику операції. Для читання відсутність відповіді зазвичай означає втрачений результат. Для запиту, що змінює дані, неприємніше інше: клієнт уже не знає, чи просто не дочекався відповіді, чи сервер встиг щось змінити.
3. Крок 2 — статус як клас проблеми
Коли ви переконалися, що відповідь справді отримано, саме час зробити те, що багато хто пропускає: подивитися на status code і сприйняти його як «клас ситуації». Це не просто цифра, а погоджений сигнал між клієнтом і сервером: «у мене вийшло», «у тебе проблема в запиті», «ресурсу немає», «конфлікт», «у мене внутрішній збій».
Важливий момент: відповідь із помилкою — це теж результат контракту. Якщо сервер відповів 400 або 404, це вже «нормальна форма спілкування»: він зміг обробити ваш запит настільки, щоб повернути структуровану відповідь, хай і з помилкою. Це відрізняється від ситуації, коли відповіді немає взагалі.
Щоб «приземлити» це на невеликий код, нам потрібен мінімальний об’єкт відповіді. Зробимо record, де є статус, content-type і body. Так, body поки що рядок: для цієї діагностики нам важливіше розрізнити статус і форму відповіді, ніж розбирати JSON по полях.
package com.example.readlater;
// Мінімальна модель HTTP-відповіді для діагностики.
// Тут важливі: статус, Content-Type і тіло (поки що рядком, без JSON-парсингу).
public record RemoteResponse(int status, String contentType, String body) {
// 2xx — клас "успіх": сервер вважає, що запит оброблено.
public boolean is2xx() {
return status >= 200 && status < 300;
}
}
Тепер можна виділити дуже просту перевірку: «статус — це помилка чи ні».
package com.example.readlater;
public class Statuses {
// 4xx/5xx — це відповідь із помилкою: відповідь є, але сервер повідомляє про помилку.
public static boolean isError(int status) {
return status >= 400 && status < 600;
}
}
Тут достатньо тримати одну опору: 404 і 500 уже належать до гілки «response отримано». Це різні проблеми, але обидві відрізняються від ситуації, де статусу немає взагалі.
Щоб було простіше тримати це в голові, ось невелика таблиця-інтерпретатор (без перетворення лекції на довідник RFC):
| Що ви спостерігаєте | Це… | Що НЕ варто робити насамперед |
|---|---|---|
| Немає response (є виняток/обрив) | Мережевий збій / недоступність | Шукати «правильний статус» і розбирати body |
| Response зі статусом 4xx | Помилка запиту / клієнтська проблема | Оголошувати «сервіс упав» (він якраз відповів) |
| Response зі статусом 5xx | Помилка на боці сервера | Вважати, що ви «точно все правильно надіслали» (інколи ви спровокували проблему) |
| Response зі статусом 2xx | Успішна доставка відповіді | Вважати, що контракт точно виконано (це наступний крок перевірки) |
У контексті проєкту ReadLater Starter це буде особливо корисно, коли ми почнемо звертатися до зовнішнього каталогу книжок. Зовнішній провайдер може чесно відповісти 404 на неправильний шлях (наприклад, ви помилилися в URL), може відповісти 400 на невалідний параметр, може відповісти 500, тому що має проблеми. І всі ці три випадки — відповіді, а не «відсутність відповіді».
4. Крок 3 — перевірка контракту
І тут усе нарешті збирається: після перевірки response і status залишається перевірити, чи справді отриманий «успіх» схожий на те, про що домовлялися.
Зараз буде момент, який часто ламає романтичне уявлення про 200 OK: інколи сервер відповідає 200, але робить це «якось не так». І це вже не просто «помилка запиту» і не «внутрішня помилка» — це часто ознака того, що контракт порушено (або ми його неправильно зрозуміли).
Контракт — це не тільки «поля в JSON». Це ще й Content-Type, і очікувана форма відповіді, і допустимі статуси для цієї кінцевої точки. Наприклад, ви очікуєте JSON, а вам приходить HTML-сторінка з текстом «Сервіс тимчасово недоступний» — і так, інколи таке прилітає зі статусом 200. Або вам приходить порожній body там, де ви очікували об’єкт. Або заголовки такі, що клієнт не може зрозуміти, як це інтерпретувати.
Давайте покажемо це на дуже приземленій перевірці: вважаємо, що «контракт схожий на JSON», якщо Content-Type — application/json, і в body є хоча б очікуваний маркер. Це не «правильна валідація», а навчальна ілюстрація ідеї: навіть успіх треба звіряти з очікуванням.
package com.example.readlater;
public class ContractChecks {
// Дуже навчальна перевірка: чи схоже це на JSON-відповідь, яку ми очікуємо.
// Це НЕ "правильний" JSON-парсер. Тут важлива ідея: навіть за 200 контракт може бути зламаний.
public static boolean looksLikeJson(RemoteResponse r) {
return "application/json".equals(r.contentType())
&& r.body() != null
// Грубий маркер: у справжньому проєкті ви б парсили JSON, а не шукали "{".
&& r.body().contains("{");
}
}
Тепер уявіть дві відповіді.
Перша — схожа на те, чого ми хочемо:
// "Нормальна" відповідь: JSON і очікувана структура.
RemoteResponse ok = new RemoteResponse(
200,
"application/json",
"""
{"items":[],"count":0}
"""
);
Друга — «успішна», але дивна:
// "Дивна" відповідь: статус 200, але Content-Type і тіло не про JSON API.
RemoteResponse weird = new RemoteResponse(
200,
"text/html",
"""
<html><body>Упс</body></html>
"""
);
Обидві формально 200, але друга порушує очікування: клієнт думав, що це JSON API, а йому прислали HTML. Це типова ситуація, коли сервіс стоїть за проксі або сторінкою помилки, або коли ви потрапили не туди (наприклад, на «людський» сайт замість API). І ваш аналіз має вміти сказати: «Статус успішний, але контракт не збігається». Це окрема категорія, і вона дуже корисна, тому що допомагає не витрачати час на «чому JSON не парситься», коли насправді JSON там і не було.
Поки нам достатньо побачити сам факт невідповідності форми. Навіть коли тіло відповіді стане для нас уже не просто рядком, а набором конкретних полів, порядок думки залишиться тим самим: спочатку response, потім status, потім збіг із домовленістю.
Ще один важливий момент: контракт може порушуватися не тільки формою body, а й статусом. Наприклад, ви очікували, що GET /books/search завжди повертає 200 (навіть якщо нічого не знайдено — порожній список), а сервіс раптом почав віддавати 404. З погляду HTTP це «можлива поведінка», але з погляду вашої домовленості — це зміна правил гри. І це потрібно бачити саме як «контракт поїхав», а не як «ну, знову 404, значить усе пропало».
5. Міні-діагностика в коді
Зараз ми зробимо маленький, але дуже показовий крок: перетворимо нашу схему на код, який класифікує результат проблемного виклику. Це не «готова бібліотека», не «фреймворк» і навіть не реальний HTTP-клієнт — це навчальний каркас мислення, який потім, коли з’явиться справжній виклик, ви просто заповните реальними даними.
Почнемо з переліку можливих підсумків. Нам достатньо чотирьох:
package com.example.readlater;
public enum CallVerdict {
// HTTP-відповіді немає (зʼєднання не встановилося, таймаут, DNS, обрив тощо)
NETWORK_FAILURE,
// HTTP-відповідь є, але статус 4xx/5xx (сервер повернув помилку на рівні протоколу).
HTTP_ERROR_RESPONSE,
// HTTP-відповідь "не схожа" на те, про що домовлялися (статус / тип / тіло).
CONTRACT_VIOLATION,
// Усе добре: є відповідь, статус успіху, контракт збігся.
SUCCESS
}
Тепер зберемо аналізатор. Він застосовуватиме рівно той порядок, який ми обговорювали: спочатку помилка або відсутність відповіді, потім статус, потім контракт.
package com.example.readlater;
public class CallAnalyzer {
public CallVerdict analyze(CallOutcome outcome) {
// 1) Найважливіше перше питання: відповідь взагалі була?
if (!outcome.hasResponse()) return CallVerdict.NETWORK_FAILURE;
RemoteResponse r = outcome.response();
// 2) Якщо статус 4xx/5xx — це "відповідь із помилкою": сервер відповів, але повідомив про помилку.
if (Statuses.isError(r.status())) return CallVerdict.HTTP_ERROR_RESPONSE;
// 3) Усе, що не потрапило в 2xx і 4xx/5xx, для цієї навчальної схеми вважаємо гілкою нестандартного статусу.
// Тут ми свідомо не розбираємо історії перенаправлення та інші спеціальні випадки.
if (!r.is2xx()) return CallVerdict.CONTRACT_VIOLATION;
// 4) Навіть за 2xx перевіряємо, що відповідь відповідає очікуванням (контракту).
if (!ContractChecks.looksLikeJson(r)) return CallVerdict.CONTRACT_VIOLATION;
return CallVerdict.SUCCESS;
}
}
Тут є навмисне спрощення: усе, що не потрапило в очікувану гілку успіху або явної помилки, ми тимчасово вважаємо unexpected для поточного контракту. Нам зараз важливий сам порядок діагностики, без окремого розбору всіх спеціальних випадків протоколу.
Зверніть увагу на навмисну простоту. Ми не будуємо «красиву» ієрархію винятків, не створюємо універсальні абстракції, не додаємо ретраї. Наша мета — щоб у вас у голові закріпився порядок питань. Цей код можна читати майже як звичайний текст: «якщо відповіді немає — мережевий збій; якщо статус помилковий — response з помилкою; якщо статус дивний — контракт; якщо форма відповіді не та — контракт; інакше успіх».
Тепер можна показати, як це виглядає в міні-демо. Так, це буде «іграшка», де ми вручну створюємо outcome, але вона чудово ілюструє класифікацію.
package com.example.readlater;
public class ReadLaterApplication {
public static void main(String[] args) {
CallAnalyzer analyzer = new CallAnalyzer();
// Імітуємо успішну відповідь: відповідь є, 200, JSON-подібне тіло.
CallOutcome ok = new CallOutcome(
new RemoteResponse(200, "application/json", """
{"items":[],"count":0}
"""),
null
);
System.out.println(analyzer.analyze(ok)); // SUCCESS
}
}
І ще один приклад — 404 (відповідь є, але помилковий статус):
package com.example.readlater;
public class Demo404 {
public static void main(String[] args) {
// 404 — це НЕ "немає відповіді", це "відповідь є, але сервер повідомляє про помилку запиту / ресурсу".
CallVerdict v = new CallAnalyzer().analyze(
new CallOutcome(
new RemoteResponse(404, "application/json", """
{"message":"не знайдено"}
"""),
null
)
);
System.out.println(v); // HTTP_ERROR_RESPONSE
}
}
І приклад «відповіді немає взагалі»:
package com.example.readlater;
public class DemoNetworkFailure {
public static void main(String[] args) {
// response == null: HTTP-відповіді немає, є лише виняток на боці клієнта / мережі.
CallOutcome out = new CallOutcome(null, new IllegalStateException("Connection refused"));
System.out.println(new CallAnalyzer().analyze(out)); // NETWORK_FAILURE
}
}
З погляду розвитку ReadLater Starter це корисно, тому що пізніше у нас зʼявиться справжній «зовнішній каталог книжок». І коли ви налагоджуватимете виклик «пошук книжки», вам знадобиться проста звичка: «Що це? Немає відповіді? Є відповідь зі статусом? Успіх, але контракт зламано?» Це набагато краще, ніж «щось не так, давайте виводити все підряд».
6. Типові помилки під час розбору викликів
Помилка №1: аналіз починається з body.
Найпопулярніша пастка: ви одразу читаєте body, намагаєтеся зрозуміти, чому там не те, а потім з’ясовується, що body — це взагалі не те, що ви думаєте, або його немає. Правильний порядок жорсткий: спочатку з’ясовуємо, чи існує response, потім статус, і лише потім тіло.
Помилка №2: 404 приймається за «сервіс недоступний».
404 — це відповідь живого сервісу. Так, вона вам не подобається, але вона означає, що мережа і сервер спрацювали достатньо добре, щоб повернути структурований сигнал. Якщо ви називаєте це «недоступністю», ви автоматично почнете шукати проблему в інфраструктурі, хоча часто це банально неправильний path або відсутній ресурс.
Помилка №3: будь-який виняток вважається 500 на боці сервера.
Виняток на боці клієнта (або у вашому застосунку) дуже часто означає «немає відповіді», а не «сервер повернув 500». У 500 є конкретна ознака: ви отримали response і побачили статус. Якщо response немає — це інший клас проблем, і його треба відокремлювати.
Помилка №4: 200 OK автоматично означає «усе добре».
200 означає, що сервер сказав «успіх», але не гарантує, що ви із сервером однаково розумієте формат відповіді. Якщо Content-Type не той, якщо body порожній, якщо форма «поїхала», це може бути контрактна проблема. І вона особливо неприємна, тому що виглядає як успіх, але ламає клієнт.
Помилка №5: запити на читання і зміну даних однаково легко повторювати.
Якщо у вас проблеми з мережею, повторити GET психологічно простіше: читання не повинне змінювати стан. А от повторювати запити на зміну даних — створення, оновлення, видалення — потрібно обережніше, тому що під час мережевого збою ви не завжди знаєте, що встиг зробити сервер. Сьогодні ми не будуємо систему повторів, але правильна обережність має з’явитися вже зараз.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ