1. Статус-код як «швидкий підсумок» запиту
Коли ви вперше починаєте працювати з HTTP, хочеться дивитися лише на тіло відповіді: «О, прийшов JSON — значить, усе добре». Але HTTP улаштований інакше. Спочатку читають перший рядок відповіді, і вже в ньому сервер коротко повідомляє сенс результату. Це як табло в аеропорту: спочатку «Delayed / On time», а деталі — потім, в оголошенні.
Цей перший рядок виглядає приблизно так:
# Перший рядок відповіді: версія протоколу + статус-код + reason phrase
HTTP/1.1 200 OK
# Тіло відповіді (body): тут сервер повернув JSON
[{"id":"42","title":"Write docs"}]
Три цифри (200) — це і є status code. Слово поруч (OK) називається reason phrase і в сучасному світі частіше потрібне людині, яка читає відповідь очима, а не програмам (програми орієнтуються на цифри). Головне правило для нас як для бекенд-розробників: статус-код — це не «прикраса» відповіді, а частина контракту. Клієнт (Postman, браузер, мобільний застосунок, інший бекенд) нерідко ухвалює рішення до того, як узагалі почне розбирати body.
Якщо зовсім по-людськи, то статус-код — це коротка відповідь на запитання: «У нас тут усе вийшло? Якщо ні, хто винен: клієнт чи сервер?». І це знання потім економить години дебагу. А іноді — місяці підтримки, тому що клієнти перестають «вгадувати», що ви мали на увазі.
Під час розбору повідомлення ми вже бачили перший рядок відповіді. Тепер важливо навчитися читати його як швидкий підсумок запиту: часто цих трьох цифр досить, щоб одразу зрозуміти, усе пройшло нормально, клієнт помилився чи проблема на боці сервера.
2. Мислення сімействами: 2xx, 4xx, 5xx
Спроба «вивчити всі статус-коди» зазвичай закінчується тим, що ви пам’ятаєте два числа (200 і 404) та легку тривогу в очах. На щастя, для нормального інженерного життя важливо спочатку навчитися мислити сімействами. У HTTP статус-кодів перша цифра — це категорія змісту, і вона вже дає 80 % розуміння, навіть якщо сам код вам незнайомий.
Найзручніший трюк — поділити код на 100 і подивитися на цілу частину. У Java це буквально одна операція:
int status = 404;
// Ціле ділення: 404 / 100 = 4 — так отримуємо «сімейство» коду.
int family = status / 100;
System.out.println(family); // 4
Щоб було наочно, тримайте мінітаблицю. Це не «енциклопедія», а саме карта місцевості:
| Сімейство | Діапазон | Як читати зміст (дуже просто) | Типове запитання, яке ви ставите |
|---|---|---|---|
| 2xx | 200–299 | Запит оброблено успішно | «Що саме вийшло і чи є body?» |
| 4xx | 400–499 | Проблема в запиті клієнта | «Що клієнт зробив не так і що виправити?» |
| 5xx | 500–599 | Проблема на боці сервера | «Що зламалося у нас і де шукати причину?» |
Так, у HTTP є ще 1xx і 3xx, і іноді ви зустрінете навіть мемний 418 I'm a teapot. Але в реальному бекенд-коді базова грамотність починається з упевненого читання 2xx / 4xx / 5xx. Це як кермування: спочатку ви вчитеся дивитися на світлофор як на «зелений / жовтий / червоний», і лише потім — розбиратися в усіх тонкощах дорожніх знаків у формі ромбика зі смужками.
Невелика функція-шпаргалка (і так, саме так іноді й налагоджують логи, особливо на старті):
String familyName(int status) {
// Визначаємо сімейство за першою цифрою (2xx, 4xx, 5xx тощо)
int family = status / 100;
// switch-вираз повертає рядок — зручний спосіб зробити «людську» мітку.
return switch (family) {
case 2 -> "успіх";
case 4 -> "помилка на боці клієнта";
case 5 -> "помилка на боці сервера";
default -> "інша група";
};
}
І перевірка «на пальцях»:
// Перевіряємо, що класифікація працює на найчастіших сімействах.
System.out.println(familyName(201)); // успіх
System.out.println(familyName(404)); // помилка на боці клієнта
System.out.println(familyName(500)); // помилка на боці сервера
3. 2xx: успіх теж буває різним
На рівні новачка є небезпечне переконання: «успіх = 200 OK». Це схоже на ідею «усі оцінки з математики — це 5». Красиво звучить, але реальність багатша. У 2xx заховані різні варіанти успішного результату, і правильний вибір робить API передбачуваним: клієнт заздалегідь розуміє, чи чекати body, чи створено новий ресурс, чи треба дивитися на заголовки.
Почнімо з трьох кодів, які нам справді знадобляться постійно: 200, 201, 204.
200 OK — «усе добре, і зазвичай (але не завжди) у відповіді є дані». Типовий приклад — отримання списку задач або однієї задачі. Навіть якщо список порожній, це все одно нормальний успіх: запит оброблено, просто даних немає.
# Успішна відповідь, тіло є (але воно порожнє: список задач без елементів)
HTTP/1.1 200 OK
[]
Або список з елементами:
# Успішна відповідь, тіло є: список задач з елементами
HTTP/1.1 200 OK
[{"id":"42","title":"Write docs"}]
201 Created — «успіх, і більше того: на сервері з’явився новий ресурс». Це прямий сигнал клієнту: «я не просто обробив запит, я створив щось нове». Часто при 201 сервер ще показує, де саме живе новий ресурс (наприклад, через заголовок Location). Про заголовки ми вже говорили в попередній лекції, тому зараз просто подивимося на форму.
# Створення ресурсу: сервер повідомляє, де він тепер доступний
HTTP/1.1 201 Created
Location: /api/v1/tasks/42
{"id":"42","title":"Напишіть документацію"}
Важливо вловити ідею: 201 — не «більш успішний 200», а інший зміст. Клієнт може поводитися інакше: наприклад, одразу перейти за Location або оновити список.
204 No Content — «успіх, але тіла відповіді немає, і це нормально». Це особливо корисно в сценаріях, де серверу справді нічого повертати й немає сенсу надсилати «порожній JSON просто за звичкою».
# Успіх без body: це нормально й очікувано для деяких операцій (наприклад, delete)
HTTP/1.1 204 No Content
Новачки часто напружуються: «як так, успіх без JSON?». А ось так: HTTP уміє говорити «усе ок» без зайвого тексту. Це як коротке «Прийнято» замість десятисторінкового звіту.
Практично корисна різниця для мозку: 200 — «успіх + (можливо) дані», 201 — «успіх + створено», 204 — «успіх + даних у відповіді немає».
4. 4xx: проблема в запиті клієнта
Коли ви бачите 4xx, перша реакція у новачка часто така: «ой, сервер повернув помилку, значить сервер зламався». Але зміст 4xx якраз протилежний: сервер працює і достатньо здоровий, щоб сказати клієнту: «друже, запит не підходить». 4xx — це не «кінець світу», а сигнал: «виправ запит».
У нашому навчальному контексті найважливіші 4xx — це 400 Bad Request і 404 Not Found. Інші коди існують, але ці два — база, без якої неможливо нормально читати відповіді.
400 Bad Request — запит загалом неправильний. Причини можуть бути різними: некоректний формат, відсутня обов’язкова частина, параметр неможливо прочитати як число тощо. На рівні «чистого HTTP» важливо одне: клієнт надіслав щось таке, що сервер не зміг коректно інтерпретувати як осмислений запит.
Уявімо, що клієнт вирішив, що page=banana — чудова ідея (банани корисні, але не в query-параметрах):
# Поганий query-параметр: сервер очікує число, а прийшов рядок
GET /api/v1/tasks?page=banana HTTP/1.1
Сервер (якщо він чесний) скаже приблизно так:
# Помилка на боці клієнта: запит зрозумілий, але некоректний за даними
HTTP/1.1 400 Bad Request
{"message":"page має бути числом"}
Так, тіло може бути будь-яким (рядок, JSON тощо). Ми зараз не обговорюємо формат помилок як систему — нам важливо, що це помилка клієнта: сервер не впав, він просто не зміг обробити запит.
404 Not Found — запит формально нормальний, але ресурс за вказаним шляхом не знайдено. Класика жанру: «дай задачу з id=42», а такої задачі немає.
# Запит коректний, але ресурс за цим шляхом відсутній
GET /api/v1/tasks/42 HTTP/1.1
Відповідь:
# Ресурс не знайдено: сервер "зрозумів", що ви хочете, але дати нічого
HTTP/1.1 404 Not Found
{"message":"Задачу 42 не знайдено"}
І ось тут важлива думка: 404 — це не про «страшну помилку», а про те, що ресурс, до якого звертаються, відсутній. Клієнт може показати користувачу «не знайдено», може прибрати елемент зі списку, може перестати намагатися працювати з цим id. Але він має розуміти саме цей зміст, а не «десь щось зламалося».
Іноді запитують: «А чому не 400? Адже клієнт же запросив неіснуючу задачу!» Тому що запит синтаксично й структурно нормальний. Клієнт попросив те, що попросив, просто цього немає. Це окрема категорія.
Якщо коротко: 400 — «я не зрозумів запит», 404 — «я зрозумів, але такого ресурсу немає».
5. 5xx і чесні статуси в API
5xx: проблема на боці сервера
Якщо 4xx — це «клієнте, виправ запит», то 5xx — це «сервер не зміг коректно обробити запит, і клієнт навряд чи може полагодити це сам». Важливо, що 5xx не завжди означає «у коді баг», але з погляду контракту виглядає саме так: помилка в обробці на серверному боці.
Найтиповіший представник сім’ї — 500 Internal Server Error. Це така «універсальна» відповідь: «щось пішло не так, ми не очікували такого повороту подій». У навчальних проєктах 500 трапляється найчастіше, тому що на початку ви ще не обробляєте помилки акуратно, і все вилітає назовні одним великим комом.
Приклад форми:
# Помилка сервера: клієнт, найімовірніше, не може «полагодити» це своїм запитом
HTTP/1.1 500 Internal Server Error
{"message":"Неочікувана помилка сервера"}
З погляду клієнта це зазвичай означає: «я надіслав запит, який загалом може бути нормальним, але сервер не впорався». Клієнт може спробувати повторити пізніше, може показати користувачу «спробуйте ще раз», може записати подію в моніторинг. Але найважливіше: клієнт не повинен удавати, що все ок.
З погляду бекенд-розробника 5xx — це привід лізти в логи й шукати, де саме сталася проблема. І тут є тонкий, але важливий момент: у 5xx-відповіді не можна «випадково» видати назовні внутрішності сервера на кшталт stack trace. По-перше, це рідко корисно клієнту. По-друге, це перетворює внутрішню реалізацію на публічний контракт, а вона ще не раз зміниться, повірте.
Статус важливіший: не ховаємо помилку в 200 OK
У початківця часто є спокуса: «щоб клієнту було простіше, я завжди повертатиму 200 OK, а всередині JSON напишу success=false і якесь повідомлення про помилку». Звучить турботливо, але на практиці це ламає контракт. HTTP — це мова спілкування, і якщо ви говорите «успіх» (2xx), а в body розповідаєте «помилка», ви створюєте API, якому не можна вірити.
Порівняймо дві відповіді на запит неіснуючої задачі.
Варіант «поганий, але популярний»:
# Статус говорить «успіх», але всередині захована помилка — це ламає контракт
HTTP/1.1 200 OK
{"error":"Task not found"}
Варіант «нудний, але чесний»:
# Статус і тіло узгоджені: це справді «не знайдено»
HTTP/1.1 404 Not Found
{"message":"Task not found"}
Обидва варіанти можуть містити текст (або JSON), але різниця радикальна: у другому випадку клієнт може ухвалити рішення за статусом, навіть не вдивляючись у текст, і це рішення буде правильним.
Чому це важливо практично, а не «тому що так написано в інтернеті»? Тому що клієнтський код часто влаштований так, що він насамперед перевіряє статус і далі розгалужується за ним. Навіть без справжнього HTTP-клієнта — просто як ідея:
void handleResponse(int status) {
// Швидка перевірка сімейства: 2xx — успіх, решту вважаємо помилкою для UX та логіки клієнта.
if (status / 100 == 2) {
System.out.println("Успіх: читаємо дані"); // Успіх: читаємо дані
return; // Важливо: далі обробка помилки вже не виконується
}
// Будь-який не-2xx: клієнт переходить у "помилкову" гілку (показати повідомлення, записати метрику тощо)
System.out.println("Помилка: показуємо повідомлення користувачу"); // Помилка: показуємо повідомлення користувачу
}
Якщо ви повертаєте 200 при помилці, клієнт потрапить у гілку «успіх» і почне читати «дані», яких немає. Далі починається цирк: клієнту доводиться парсити body, шукати в ньому error, порівнювати рядки, вгадувати формати… і все це замість того, щоб використовувати стандартний, очікуваний механізм HTTP.
Окрема біда — інструменти та бібліотеки. Багато HTTP-клієнтів (і інтеграцій) автоматично вважають 4xx/5xx помилкою, піднімають виняток, пишуть метрики, додають подію в моніторинг. А якщо ви повертаєте 200, то для цих інструментів усе чудово, «зелена галочка», і ви самі собі вимикаєте сигналізацію в домі, тому що «вона голосно пищала».
Тому правило просте і майже без гумору: статус-код має відповідати змісту результату. А body — це місце для деталей, якщо вони потрібні.
6. Типові помилки під час роботи зі статус-кодами
Помилка №1: «Завжди повертаємо 200 OK, а помилки пишемо текстом у body». Такий підхід здається зручним у моменті, тому що клієнту наче не потрібно розбиратися в статусах. Але насправді ви ламаєте базову механіку HTTP і змушуєте всіх клієнтів вгадувати формат помилок. Нормальний шлях — розрізняти успіх (2xx) і помилку (4xx/5xx) на рівні статусу, а тіло використовувати як пояснення.
Помилка №2: плутанина між 4xx і 5xx. Іноді сервер відповідає 500, коли клієнт передав відверто неправильний запит, і навпаки — відповідає 400, коли у сервера справді баг. У результаті клієнт не розуміє, чи треба йому виправляти запит, чи просто почекати, а команда розробників не розуміє, де шукати проблему. Правильна звичка — спочатку ставити собі запитання «хто може це виправити: клієнт чи сервер?», і лише потім обирати сімейство.
Помилка №3: вважати, що «успіх» завжди має містити JSON-відповідь. Через цю звичку люди бояться 204 No Content і починають повертати «порожні об’єкти заради пристойності». Але порожнє тіло за успішного результату — нормальна частина HTTP. Якщо повертати нічого, краще чесно сказати 204, ніж удавати, що «дані є», але вони чомусь {}.
Помилка №4: зловживати «трохи рідкіснішими» кодами до розуміння бази. HTTP справді багатий: там є і 202, і 206, і десятки інших кодів. Є навіть легендарний 418, який добре виглядає в мемах. Але в звичайному прикладному API спершу потрібно впевнено тримати основу (200/201/204/400/404/500) і розуміти їхній зміст. І лише потім розширювати палітру, коли це справді потрібно.
Помилка №5: вважати, що статус-код — «для людини», а клієнт усе одно читає body. У реальності все навпаки: статус-код насамперед потрібний клієнтському коду й інструментам, а reason phrase і текст — людині. Якщо клієнтами вашого API є інші сервіси або мобільні застосунки, вони не будуть «читати очима» ваш JSON — вони розгалужуватимуться за статусами. І якщо статус «бреше», це ламає інтеграцію швидше за будь-яке некрасиве поле в JSON.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ