Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Обработка ошибок и 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/99
HTTP/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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.