Логотип Workflow

Article

Updated at:

Stage 12: API Error Handling with ControllerAdvice

Этап 12 - Обработка API ошибок через ControllerAdvice

Обработка ошибок становится заметной в первый момент, когда frontend-разработчик спрашивает: “Что именно мне показать пользователю, если этот request упал?” Если каждый controller возвращает ошибку в своем формате, frontend не может сделать один нормальный компонент ошибок. Один endpoint возвращает plain text, другой HTML error page, третий stack trace, а validation errors приходят вообще в другой форме. Backend технически может работать, но API получается неприятным и ненадежным.

В Spring есть более правильная модель: controllers описывают успешные HTTP-действия, services выбрасывают понятные exceptions, когда use case нельзя выполнить, а один global handler переводит эти exceptions в стабильный JSON error contract. Такой global handler обычно помечается @RestControllerAdvice. Он перехватывает exceptions из controllers и превращает их в ResponseEntity с правильным HTTP status и response body.

Этап 12 - Обработка API ошибок через ControllerAdvice

Последовательность request

Последовательность простая. Request приходит в OrderController. Controller вызывает orderService.cancel(orderId). Service загружает order, проверяет ownership и текущий status, затем выбрасывает OrderNotFoundException, AccessDeniedException или OrderAlreadyShippedException, если операция невозможна. Controller не ловит эти exceptions. Они уходят в advice class, где каждый тип exception получает понятный status и JSON body.

СитуацияHTTP statusЗначение
Сломанный JSON или неверное поле400 Bad RequestКлиент отправил данные, которые API не может принять.
Order id не существует404 Not FoundЗапрошенный ресурс отсутствует.
Order принадлежит другому user403 ForbiddenПользователь известен, но не имеет доступа.
Order уже отправлен409 ConflictЗапрос конфликтует с текущим бизнес-состоянием.
Неожиданный баг или outage базы500 Internal Server ErrorСервер упал и не должен раскрывать внутренности.

Конкретный Spring пример

public record ApiError(
    String code,
    String message,
    List<FieldErrorDto> fieldErrors
) {}

@RestControllerAdvice
class ApiExceptionHandler {
    @ExceptionHandler(OrderNotFoundException.class)
    ResponseEntity<ApiError> notFound(OrderNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ApiError("ORDER_NOT_FOUND", ex.getMessage(), List.of()));
    }

    @ExceptionHandler(OrderAlreadyShippedException.class)
    ResponseEntity<ApiError> conflict(OrderAlreadyShippedException ex) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(new ApiError("ORDER_ALREADY_SHIPPED", ex.getMessage(), List.of()));
    }
}

Validation errors

Validation errors требуют еще один handler, потому что Spring выбрасывает MethodArgumentNotValidException до входа в тело controller method. В этом handler ты читаешь field errors и возвращаешь список вида field=quantity, message=must be greater than or equal to 1. Это намного полезнее, чем общий “Bad Request”.

Главное правило: error body является частью API contract. Не клади случайные имена exception classes в code. Не раскрывай SQL messages. Не возвращай 200 OK с полем error внутри JSON. Используй HTTP status для transport meaning, а stable code - для business meaning.

Частые ошибки

  • Ловить exceptions в каждом controller и возвращать разные response shapes.
  • Отдавать clients stack traces, SQL errors или имена Java exceptions.
  • Использовать 500 для business problems вроде “order already shipped”.
  • Возвращать 200 OK для failed operations, потому что так проще написать controller.

Чеклист понимания

  • Я могу связать типичные API failures с правильным HTTP status.
  • Я могу объяснить, почему @RestControllerAdvice находится вне отдельных controllers.
  • Я могу спроектировать стабильный JSON error body для frontend и integration clients.

Вопросы для самопроверки

  1. Почему service должен выбрасывать business exception, а не возвращать null?
  2. Что должен содержать validation error response?
  3. Почему stack trace опасен в public API response?