1. Відповіді Boot під час помилок
Помилки у вебсервісі звучать похмуро, але на практиці це просто ще один тип відповіді, який сервіс має вміти повертати. Майже як «службове повідомлення» від застосунку: «Я зрозумів запит, але виконати його не можу». І Spring Boot у цьому сенсі досить уважний: він не залишає клієнта наодинці з порожнечею.
Важливо подумки перейти від «помилка = катастрофа» до «помилка = нормальний сценарій протоколу». В HTTP прямо закладено, що відповідь може бути успішною (2xx) або неуспішною (4xx і 5xx). І коли ви будуєте API (навіть навчальний), клієнт регулярно бачитиме обидва варіанти. Тому наша мета сьогодні не «написати красиву систему помилок», а хоча б упевнено розуміти, що Boot робить за замовчуванням.
Успішний JSON і JSON помилки
Коли ви повертаєте CourseCard, ви повертаєте дані предметної області: курси, треки, рівні, ціни. Коли виникає помилка, предметної області може не бути взагалі — сервіс уже не обговорює курси, він повідомляє про проблему. Тому в помилки буде інша форма JSON, і це нормально. Ба більше, було б дивно, якби помилка мала форму CourseCard.
Найважливіший сигнал помилки — не body, а HTTP-статус. Тіло відповіді — це «пояснення», а статус — «офіційна відповідь». Якщо ви навчитеся починати розбір проблеми зі статусу, ви заощадите собі безліч часу і нервів (і, можливо, врятуєте клавіатуру від драматичного удару чолом).
Для орієнтиру тримайте в голові таку картину:
| Що бачить клієнт | Що це означає простою мовою | Приклад сценарію в catalog-service |
|---|---|---|
| 200 OK | «Усе нормально, тримай дані» | Ви звернулися до /api/catalog/courses і отримали список |
| 400 Bad Request | «Запит зрозумілий, але параметри або формат некоректні» | Ви передали limit=abc (а очікувалося число) |
| 404 Not Found | «Такого шляху або ресурсу немає» | Ви запросили неіснуючий URL |
| 500 Internal Server Error | «На сервері щось зламалося» | Усередині контролера або сервісу сталася необроблена виняткова ситуація |
На цьому рівні ми свідомо не занурюємося в десятки статусів. Наша задача — побачити базові категорії: 4xx частіше за все означає «помилився клієнт», а 5xx — «помилився сервер».
Іншими словами, якщо у відповіді прийшов JSON, це ще не означає успіх. JSON може бути і в помилки. Тому насамперед завжди дивіться на статус, а вже потім — на body.
У контексті Boot це особливо важливо, тому що стандартна відповідь з помилкою часто виглядає акуратно і дуже схожа на звичайний JSON, навіть якщо всередині захована серйозна проблема.
Звідки береться /error
Spring Boot не просто «віддає JSON з контролера». Він будує цілу інфраструктуру навколо обробки запитів. І коли щось іде не так — неправильний шлях, виняток, помилка конвертації параметра — Boot намагається звести це до єдиної точки, де можна сформувати зрозумілу відповідь. Такою точкою часто стає обробник /error, який Boot реєструє автоматично.
Якщо говорити дуже простими словами, Spring MVC намагається виконати ваш контролер, а якщо не виходить, то далі в гру вступає механізм обробки помилок. Boot запускає стандартний ланцюжок обробки помилок, і вже він вирішує, що саме відправити клієнту: HTML чи JSON, який статус виставити, які поля включити.
Ось спрощена схема, щоб перестати думати «воно просто саме»:
flowchart TD
A[HTTP запит] --> B[DispatcherServlet]
B --> C{Знайдено обробник?}
C -- ні --> E[Помилка: 404]
C -- так --> D[Виклик методу контролера]
D --> F{Усе гаразд?}
F -- так --> G[Jackson -> JSON]
F -- ні, exception --> H[Помилка: 500 або інша]
E --> I["/error"]
H --> I["/error"]
I --> J{"Accept: JSON чи HTML?"}
J -- JSON --> K[JSON помилки]
J -- HTML --> L["HTML-помилка (whitelabel або сторінка)"]
Із цієї схеми корисно винести дві речі. По-перше, помилка може статися навіть до виклику вашого методу, наприклад на етапі пошуку маршруту або конвертації параметрів. По-друге, відповідь про помилку формується інфраструктурою Boot, тому вона може виглядати однаково для різних типів проблем.
2. HTML чи JSON: content negotiation
На практиці початківець часто ловить дивне відчуття: «Я ж роблю JSON API, чому в браузері мені прилітає якась HTML-сторінка з повідомленням про помилку?» Це не магія і не зрада, а звичайний вебсвіт. Клієнти бувають різні: браузер, Postman, curl, мобільний застосунок. І вони по-різному просять формат відповіді.
Spring Boot робить вибір за заголовком Accept. Якщо клієнт каже: «Мені потрібен HTML», Boot спробує дати HTML. Якщо клієнт каже: «Мені потрібен JSON», Boot віддасть JSON. У браузері за замовчуванням часто очікують HTML, тому ви бачите HTML-сторінку помилки. У Postman зазвичай ви явно працюєте як API-клієнт і найчастіше отримуєте JSON.
Спробуйте побачити це на практиці на будь-якому помилковому URL. Наприклад, запитайте неіснуючий шлях:
# Просимо JSON, щоб Boot повернув JSON-помилку, а не HTML-сторінку
curl -i -H "Accept: application/json" http://localhost:8080/this-page-does-not-exist
У відповіді ви побачите статус 404 і Content-Type, пов'язаний із JSON (application/json або близький до нього). А тепер той самий запит, але з HTML:
# Просимо HTML — у браузерному сценарії зазвичай саме цього й очікують
curl -i -H "Accept: text/html" http://localhost:8080/this-page-does-not-exist
Тепер ви, найімовірніше, отримаєте HTML-сторінку помилки. Це один і той самий факт помилки, просто різне представлення.
Дуже важлива думка: HTML-помилка в браузері не означає, що ваш API «не JSON». Це означає, що конкретний клієнт (браузер) попросив HTML. А ваш JSON-клієнт, наприклад фронтенд, мобільний застосунок або інший сервіс, бачитиме JSON.
3. Три типові сценарії помилок
Помилки бувають різними, але на рівні базового Boot-сервісу корисно вміти впізнавати найчастіші. Тут ми подивимося на три сценарії: неправильний URL (404), неправильний тип параметра (400) і необроблений виняток (500). Це саме ті ситуації, які ви бачитимете найчастіше, поки сервіс маленький і навчальний.
404 Not Found: маршрут не існує
Найпростіший спосіб отримати помилку — перейти туди, де в сервісу немає кінцевої точки. Наприклад:
# Переходимо на неіснуючий маршрут — отримуємо 404 від інфраструктури Boot
curl -i -H "Accept: application/json" http://localhost:8080/api/catalog/abracadabra
Якщо такого маршруту ви не реєстрували, буде 404. І Boot сформує стандартну відповідь з помилкою. Поля можуть трохи відрізнятися залежно від версії та налаштувань, але ідея залишиться тією самою: статус, шлях і, можливо, час.
Це важливо розуміти методично: 404 — це не «сервер зламався», а найчастіше «ви пішли не туди». У реальному житті це або баг у клієнті (не той URL), або просто ручне тестування «пальцем у небо».
400 Bad Request: параметр не того типу
Тепер цікавіший випадок. Ви часто приймаєте числа в параметрах запиту (limit) або очікуєте LocalDate чи enum у фільтрах. Якщо клієнт передав значення, яке неможливо конвертувати, Spring MVC може навіть не дійти до коду вашого методу — помилка станеться раніше.
Спрощений приклад:
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController // Кажемо Spring, що це REST-контролер: відповіді буде серіалізовано в JSON
class CourseCatalogController {
@GetMapping("/api/catalog/courses") // Реєструємо маршрут
public List<CourseCard> courses(@RequestParam int limit) { // limit має конвертуватися в int
// Важливо: якщо limit не число, сюди ми взагалі не потрапимо — Spring поверне 400 раніше
return List.of();
}
}
Тепер запит:
# Передаємо limit=abc — Spring не зможе сконвертувати його в int і поверне 400
curl -i -H "Accept: application/json" "http://localhost:8080/api/catalog/courses?limit=abc"
Тут limit=abc не перетворюється на int. Тому ви побачите 400 Bad Request і стандартний JSON помилки.
Корисне спостереження: 400 — це майже завжди «клієнте, ви надіслали щось дивне». Навіть якщо клієнтом сьогодні є ви самі і ви «просто тестували». Так, це буває боляче. Але зате чесно.
500 Internal Server Error: ми самі впали
Найдраматичніший статус — 500. Він означає, що запит загалом був нормальний (URL існує, параметри розібралися), але всередині обробки сталося необроблене завершення через виняток.
Для демонстрації можна завести маленький debug endpoint. У навчальному проєкті це допустимо як тимчасовий «стенд», щоб один раз побачити поведінку наочно.
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // Технічний контролер для демонстрації падіння
class DebugController {
@GetMapping("/api/catalog/fail") // Спеціальна кінцева точка, яка завжди падає
public Map<String, String> fail() {
// Виняток не перехоплено -> Boot сформує 500 і стандартну відповідь з помилкою
throw new IllegalStateException("Каталог не готовий");
}
}
І тепер запит:
# Викликаємо кінцеву точку, яка кидає виняток — очікуємо 500
curl -i -H "Accept: application/json" http://localhost:8080/api/catalog/fail
Відповіддю буде 500 Internal Server Error і стандартна відповідь з помилкою.
Самоіронічна правда backend-розробника: 500 — це не «сервер втомився», а «ми накосячили». Іноді це чесна і випадкова помилка, іноді — дуже творча. Але в будь-якому разі це сигнал: проблему треба шукати в коді сервісу й у логах, а не в параметрах запиту.
4. Як читати відповідь з помилкою
Коли бачите помилку, хочеться відразу дивитися на JSON body і намагатися «зрозуміти по тексту». Це природно, але неефективно. Набагато краще виробити маленький ритуал: спочатку статус, потім шлях, потім те, що саме ви надіслали. І лише після цього — деталі в тілі відповіді, якщо вони є.
Давайте розкладемо «читання помилки» як звичку.
Спочатку ви дивитеся на HTTP status. Якщо це 404, ви перевіряєте URL. Якщо це 400, ви перевіряєте параметри: типи, формат дат, enum-значення, друкарські помилки. Якщо це 500, ви майже напевно йдете в логи застосунку, тому що клієнт зазвичай не повинен бачити stack trace та внутрішні деталі.
Потім ви дивитеся на path у відповіді з помилкою. Це допомагає швидко зрозуміти, яка саме кінцева точка «болить», особливо якщо у вас кілька запитів в історії.
І лише потім ви читаєте поля в JSON, але з правильним очікуванням: Boot за замовчуванням часто не розкриває занадто багато деталей, і це плюс. Він не зобов’язаний розповідати зовнішньому світу про ваші винятки, класи, stack trace тощо. Це не «поганий UX», а мінімальна безпека і здоровий глузд.
До речі, важливий зв’язок із темою базового JSON-рівня: відповідь з помилкою також серіалізується в JSON, у більшості випадків через Jackson. Тому якщо ви робили глобальні JSON-налаштування, вони потенційно можуть впливати і на формат помилки. Але на рівні курсу це просто цікавий факт, а не привід терміново конструювати «ідеальні помилки».
5. Межа курсу: без єдиного контракту помилок
Після знайомства зі стандартною відповіддю з помилкою часто виникає бажання: «Давайте зробимо красиво! Щоб було однаково! Щоб у всіх помилок були свої code, details, requestId, і взагалі як у великих дорослих системах!» Бажання хороше. Проблема лише в тому, що це вже окрема дисципліна, і вона легко перетворюється на нескінченну тему.
У межах курсу Spring Boot наша мета — платформа і базовий рівень, а не повноцінний REST-дизайн. Повноцінна стратегія помилок — це і про контракт API, і про валідацію, і про класифікацію винятків, і про безпеку повідомлень, і про підтримку клієнтів, і про документацію. Це цілком тягне на окремий великий блок.
Тому тут ми свідомо зупиняємося на простому й чесному рівні: ви маєте вміти розпізнати стандартну поведінку Boot і не намагатися переписати її «бо некрасиво». На ранній стадії проєкту важливіші стабільний запуск, зрозумілі контролери і передбачуваний JSON успіху. А красиві помилки ми залишимо на правильний момент, щоб не перетворювати Boot на «курс про все підряд».
6. Типові помилки під час роботи з відповіддю з помилкою
Помилка №1: плутати успішний JSON і JSON помилки як одну й ту саму «форму відповіді».
Іноді початківець дивиться на JSON у відповіді й думає: «О, JSON прийшов — значить контролер спрацював». Але JSON може бути і в помилки. Тому завжди починайте з HTTP-статусу: 200 — це контракт успіху (наприклад, CourseCard), 4xx/5xx — це контракт помилки.
Помилка №2: ігнорувати HTTP-статус і аналізувати лише body.
Це класика жанру: клієнт бачить JSON, читає поле error і робить висновки у стилі «щось не так». Але конкретика живе у статусі й у тому, як ви сформували запит. Тіло відповіді при помилці може бути мінімальним, і це нормально. Статус — головний сигнал протоколу.
Помилка №3: намагатися «полагодити» помилку на боці сервера, коли проблема в запиті.
Якщо ви отримали 400, це дуже часто означає: «Я не зміг розібрати або сконвертувати параметри». І замість того, щоб виправити limit=abc на limit=10, розробник починає змінювати код контролера, додавати зайві try/catch і підозрювати Jackson у всіх бідах. У більшості таких випадків помилка вирішується виправленням запиту або типів параметрів, а не переписуванням сервісу.
Помилка №4: сприймати будь-який 500 як «ну гаразд, буває» і не шукати причину.
500 — це майже завжди баг або незакритий сценарій у коді. Навіть якщо ви спеціально зробили кінцеву точку, яка падає, у реальному застосунку 500 має вас насторожити. Стандартний JSON помилки тут не зобов’язаний пояснювати «чому» — він просто повідомляє факт. Справжні деталі лежать у логах.
Помилка №5: занадто рано намагатися замінити стандартний механізм помилок «на власний», не зрозумівши baseline.
Дуже хочеться одразу зробити @ControllerAdvice, власні DTO для помилок, красивий errorCode і тисячі полів «про всяк випадок». Підсумок зазвичай сумний: застосунок стає складнішим, а розуміння Boot — меншим. Набагато здоровіше спочатку прийняти стандартний механізм, навчитися його впізнавати і лише потім, уже в REST-шарі, робити свідому архітектуру помилок.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ