Обработка ошибок и ProblemDetail
Сейчас запрос несуществующей статьи заканчивается ошибкой 500, а ответ о неверных данных не говорит, какое поле не прошло проверку. Хороший API отвечает на ошибки так же аккуратно, как на успешные запросы. В Spring для этого есть обработчики исключений и класс ProblemDetail — стандартный формат описания ошибки (RFC 9457).
Класс-обработчик
Создайте класс, который перехватывает исключения из всех контроллеров:
// src/main/java/com/example/blog/ApiExceptionHandler.java
package com.example.blog;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(PostNotFoundException.class)
public ProblemDetail handleNotFound(PostNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problem.setTitle("Статья не найдена");
return problem;
}
}@RestControllerAdvice делает класс общим помощником для всех контроллеров. Метод с @ExceptionHandler(PostNotFoundException.class) вызывается, когда любой контроллер выбрасывает это исключение, и его результат уходит клиенту вместо ошибки 500.
Что получит клиент
curl -i http://localhost:8080/api/posts/99HTTP/1.1 404
Content-Type: application/problem+json
{"detail":"Статья 99 не найдена","instance":"/api/posts/99","status":404,"title":"Статья не найдена"}Тип application/problem+json подсказывает клиенту, что это описание ошибки. Поля стандартные:
| Поле | Значение |
|---|---|
status | Код статуса |
title | Короткое название ошибки |
detail | Подробности для этого случая |
instance | Адрес запроса, где случилась ошибка |
Ошибки проверки по полям
Добавим в тот же класс второй метод — для исключения, которое Spring выбрасывает при неудачной проверке @Valid:
// src/main/java/com/example/blog/ApiExceptionHandler.java
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new TreeMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Ошибка проверки данных");
problem.setProperty("errors", errors);
return problem;
}getFieldErrors() возвращает список ошибок с именем поля и сообщением. setProperty добавляет в ответ своё поле errors. TreeMap сортирует поля по алфавиту, чтобы ответ был стабильным.
curl -X POST http://localhost:8080/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"","body":"x"}'{"detail":"Ошибка проверки данных","instance":"/api/posts","status":400,"title":"Bad Request","errors":{"author":"не должно быть пустым","title":"не должно быть пустым"}}Теперь клиент видит, что нужно исправить два поля.
Частая ошибка
В Spring Boot есть настройка spring.mvc.problemdetails.enabled: true — она переводит в формат ProblemDetail все стандартные ошибки Spring. Но вместе с ней включается встроенный обработчик, и он перехватывает MethodArgumentNotValidException раньше вашего метода. В ответе снова не будет списка полей:
{"detail":"Invalid request content.","instance":"/api/posts","status":400,"title":"Bad Request"}Выберите что-то одно: либо своя обработка ошибок проверки, как в этом уроке, либо настройка. Если нужны обе вещи, класс-обработчик наследуют от ResponseEntityExceptionHandler и переопределяют его метод handleMethodArgumentNotValid.