JavaRush /Курси /Spring Boot /Стандартний JSON помилок у Spring Boot

Стандартний JSON помилок у Spring Boot

Spring Boot
Рівень 12 , Лекція 4
Відкрита

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-шарі, робити свідому архітектуру помилок.

1
Опитування
Spring JSON, рівень 12, лекція 4
Недоступний
Spring JSON
Серіалізація відповідей і помилки
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ