Рубрики
Без рубрики

Пользовательская обработка сообщений об ошибках для REST API

Реализуйте глобальный обработчик исключений для API REST с помощью Spring.

Автор оригинала: baeldung.

1. Обзор

В этом уроке мы обсудим, как реализовать глобальный обработчик ошибок для API Spring REST.

Мы будем использовать семантику каждого исключения для создания значимых сообщений об ошибках для клиента с четкой целью предоставить этому клиенту всю информацию, чтобы легко диагностировать проблему.

Дальнейшее чтение:

Spring ResponseStatusException

Обработка ошибок для отдыха с пружиной

2. Пользовательское Сообщение Об Ошибке

Давайте начнем с реализации простой структуры для отправки ошибок по проводу – ApiError :

public class ApiError {

    private HttpStatus status;
    private String message;
    private List errors;

    public ApiError(HttpStatus status, String message, List errors) {
        super();
        this.status = status;
        this.message = message;
        this.errors = errors;
    }

    public ApiError(HttpStatus status, String message, String error) {
        super();
        this.status = status;
        this.message = message;
        errors = Arrays.asList(error);
    }
}

Информация здесь должна быть простой:

  • статус : код состояния HTTP
  • сообщение : сообщение об ошибке, связанное с исключением
  • ошибка : Список построенных сообщений об ошибках

И, конечно, для реальной логики обработки исключений весной мы будем использовать аннотацию @ControllerAdvice :

@ControllerAdvice
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {
    ...
}

3. Обработка Исключений Плохих Запросов

3.1. Обработка исключений

Теперь давайте посмотрим, как мы можем справиться с наиболее распространенными ошибками клиента – в основном сценариями, когда клиент отправил неверный запрос в API:

  • BindException : Это исключение возникает при возникновении фатальных ошибок привязки.
  • MethodArgumentNotValidException : Это исключение возникает, когда аргумент с аннотацией @Valid не прошел проверку:

@Override
protected ResponseEntity handleMethodArgumentNotValid(
  MethodArgumentNotValidException ex, 
  HttpHeaders headers, 
  HttpStatus status, 
  WebRequest request) {
    List errors = new ArrayList();
    for (FieldError error : ex.getBindingResult().getFieldErrors()) {
        errors.add(error.getField() + ": " + error.getDefaultMessage());
    }
    for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
        errors.add(error.getObjectName() + ": " + error.getDefaultMessage());
    }
    
    ApiError apiError = 
      new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
    return handleExceptionInternal(
      ex, apiError, headers, apiError.getStatus(), request);
}

Как вы можете видеть, мы переопределяем базовый метод из ResponseEntityExceptionHandler и предоставляем нашу собственную пользовательскую реализацию .

Это не всегда будет так – иногда нам придется обрабатывать пользовательское исключение, которое не имеет реализации по умолчанию в базовом классе, как мы увидим позже здесь.

Следующий:

  • MissingServletRequestPartException : Это исключение возникает, когда часть составного запроса не найдена

  • MissingServletRequestParameterException : Это исключение возникает при запросе отсутствующего параметра:

@Override
protected ResponseEntity handleMissingServletRequestParameter(
  MissingServletRequestParameterException ex, HttpHeaders headers, 
  HttpStatus status, WebRequest request) {
    String error = ex.getParameterName() + " parameter is missing";
    
    ApiError apiError = 
      new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
    return new ResponseEntity(
      apiError, new HttpHeaders(), apiError.getStatus());
}
  • Исключение нарушения ограничений : Это исключение сообщает о результате нарушения ограничений:

@ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity handleConstraintViolation(
  ConstraintViolationException ex, WebRequest request) {
    List errors = new ArrayList();
    for (ConstraintViolation violation : ex.getConstraintViolations()) {
        errors.add(violation.getRootBeanClass().getName() + " " + 
          violation.getPropertyPath() + ": " + violation.getMessage());
    }

    ApiError apiError = 
      new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
    return new ResponseEntity(
      apiError, new HttpHeaders(), apiError.getStatus());
}
  • TypeMismatchException : Это исключение возникает при попытке установить свойство bean с неправильным типом.

  • Аргумент метода TypeMismatchException : Это исключение возникает, когда аргумент метода не является ожидаемым типом:

@ExceptionHandler({ MethodArgumentTypeMismatchException.class })
public ResponseEntity handleMethodArgumentTypeMismatch(
  MethodArgumentTypeMismatchException ex, WebRequest request) {
    String error = 
      ex.getName() + " should be of type " + ex.getRequiredType().getName();

    ApiError apiError = 
      new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
    return new ResponseEntity(
      apiError, new HttpHeaders(), apiError.getStatus());
}

3.2. Использование API от Клиента

Давайте теперь посмотрим на тест, который запускает Аргумент метода TypeMismatchException : мы отправим запрос с id как String вместо long :

@Test
public void whenMethodArgumentMismatch_thenBadRequest() {
    Response response = givenAuth().get(URL_PREFIX + "/api/foos/ccc");
    ApiError error = response.as(ApiError.class);

    assertEquals(HttpStatus.BAD_REQUEST, error.getStatus());
    assertEquals(1, error.getErrors().size());
    assertTrue(error.getErrors().get(0).contains("should be of type"));
}

И, наконец, – рассмотрение этой же просьбы::

Request method:	GET
Request path:	http://localhost:8080/spring-security-rest/api/foos/ccc

Вот как будет выглядеть такой ответ на ошибку JSON:

{
    "status": "BAD_REQUEST",
    "message": 
      "Failed to convert value of type [java.lang.String] 
       to required type [java.lang.Long]; nested exception 
       is java.lang.NumberFormatException: For input string: \"ccc\"",
    "errors": [
        "id should be of type java.lang.Long"
    ]
}

4. Обработайте исключение NoHandlerFoundException

Затем мы можем настроить ваш сервлет так, чтобы он создавал это исключение вместо отправки ответа 404 – следующим образом:


    api
    
      org.springframework.web.servlet.DispatcherServlet        
    
        throwExceptionIfNoHandlerFound
        true
    

Затем, как только это произойдет, мы сможем просто справиться с этим, как с любым другим исключением:

@Override
protected ResponseEntity handleNoHandlerFoundException(
  NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
    String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL();

    ApiError apiError = new ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error);
    return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus());
}

Вот простой тест:

@Test
public void whenNoHandlerForHttpRequest_thenNotFound() {
    Response response = givenAuth().delete(URL_PREFIX + "/api/xx");
    ApiError error = response.as(ApiError.class);

    assertEquals(HttpStatus.NOT_FOUND, error.getStatus());
    assertEquals(1, error.getErrors().size());
    assertTrue(error.getErrors().get(0).contains("No handler found"));
}

Давайте взглянем на полный запрос:

Request method:	DELETE
Request path:	http://localhost:8080/spring-security-rest/api/xx

И ответ error JSON:

{
    "status":"NOT_FOUND",
    "message":"No handler found for DELETE /spring-security-rest/api/xx",
    "errors":[
        "No handler found for DELETE /spring-security-rest/api/xx"
    ]
}

5. Обработайте исключение httprequestmethod Not Supportedexception

Далее давайте рассмотрим еще одно интересное исключение – исключение HttpRequestMethodNotSupportedException , которое возникает, когда вы отправляете запрос с неподдерживаемым методом HTTP:

@Override
protected ResponseEntity handleHttpRequestMethodNotSupported(
  HttpRequestMethodNotSupportedException ex, 
  HttpHeaders headers, 
  HttpStatus status, 
  WebRequest request) {
    StringBuilder builder = new StringBuilder();
    builder.append(ex.getMethod());
    builder.append(
      " method is not supported for this request. Supported methods are ");
    ex.getSupportedHttpMethods().forEach(t -> builder.append(t + " "));

    ApiError apiError = new ApiError(HttpStatus.METHOD_NOT_ALLOWED, 
      ex.getLocalizedMessage(), builder.toString());
    return new ResponseEntity(
      apiError, new HttpHeaders(), apiError.getStatus());
}

Вот простой тест, воспроизводящий это исключение:

@Test
public void whenHttpRequestMethodNotSupported_thenMethodNotAllowed() {
    Response response = givenAuth().delete(URL_PREFIX + "/api/foos/1");
    ApiError error = response.as(ApiError.class);

    assertEquals(HttpStatus.METHOD_NOT_ALLOWED, error.getStatus());
    assertEquals(1, error.getErrors().size());
    assertTrue(error.getErrors().get(0).contains("Supported methods are"));
}

И вот полный запрос:

Request method:	DELETE
Request path:	http://localhost:8080/spring-security-rest/api/foos/1

И ответ на ошибку JSON:

{
    "status":"METHOD_NOT_ALLOWED",
    "message":"Request method 'DELETE' not supported",
    "errors":[
        "DELETE method is not supported for this request. Supported methods are GET "
    ]
}

6. Обработайте исключение HttpMediaTypeNotSupportedException

Теперь давайте разберемся HttpMediaTypeNotSupportedException – который возникает, когда клиент отправляет запрос с неподдерживаемым типом носителя – следующим образом:

@Override
protected ResponseEntity handleHttpMediaTypeNotSupported(
  HttpMediaTypeNotSupportedException ex, 
  HttpHeaders headers, 
  HttpStatus status, 
  WebRequest request) {
    StringBuilder builder = new StringBuilder();
    builder.append(ex.getContentType());
    builder.append(" media type is not supported. Supported media types are ");
    ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", "));

    ApiError apiError = new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, 
      ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2));
    return new ResponseEntity(
      apiError, new HttpHeaders(), apiError.getStatus());
}

Вот простой тест, связанный с этой проблемой:

@Test
public void whenSendInvalidHttpMediaType_thenUnsupportedMediaType() {
    Response response = givenAuth().body("").post(URL_PREFIX + "/api/foos");
    ApiError error = response.as(ApiError.class);

    assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, error.getStatus());
    assertEquals(1, error.getErrors().size());
    assertTrue(error.getErrors().get(0).contains("media type is not supported"));
}

Наконец, вот пример запроса:

Request method:	POST
Request path:	http://localhost:8080/spring-security-
Headers:	Content-Type=text/plain; charset=ISO-8859-1

И ответ на ошибку JSON:

{
    "status":"UNSUPPORTED_MEDIA_TYPE",
    "message":"Content type 'text/plain;charset=ISO-8859-1' not supported",
    "errors":["text/plain;charset=ISO-8859-1 media type is not supported. 
       Supported media types are text/xml 
       application/x-www-form-urlencoded 
       application/*+xml 
       application/json;charset=UTF-8 
       application/*+json;charset=UTF-8 */"
    ]
}

7. Обработчик по умолчанию

Наконец, давайте реализуем резервный обработчик-логику типа catch – all, которая имеет дело со всеми другими исключениями, не имеющими определенных обработчиков:

@ExceptionHandler({ Exception.class })
public ResponseEntity handleAll(Exception ex, WebRequest request) {
    ApiError apiError = new ApiError(
      HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred");
    return new ResponseEntity(
      apiError, new HttpHeaders(), apiError.getStatus());
}

8. Заключение

Создание правильного, зрелого обработчика ошибок для API Spring REST – сложный и, безусловно, итеративный процесс. Надеюсь, этот учебник станет хорошей отправной точкой для этого для вашего API, а также хорошим ориентиром для того, как вы должны смотреть на то, чтобы помочь клиентам вашего API быстро и легко диагностировать ошибки и проходить мимо них.

полную реализацию этого учебника можно найти в проекте Github – это проект на основе Eclipse, поэтому его должно быть легко импортировать и запускать как есть.