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

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

1. обзор

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

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

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

Spring ResponseStatusException

Узнайте, как применять коды состояния к HTTP-ответам в Spring с помощью ResponseStatusException.

Read more

Обработка ошибок для REST с помощью Spring

Обработка исключений для REST API - проиллюстрируйте новый рекомендуемый подход Spring 3.2, а также более ранние решения.

Read more

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);
    }
}

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

  • status: код состояния HTTP

  • message: сообщение об ошибке, связанное с исключением

  • error: Список созданных сообщений об ошибках

И, конечно же, для фактической логики обработки исключений в Springwe’ll use аннотация@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);
}


Как видите,we are overriding a base method out of the ResponseEntityExceptionHandler and providing our own custom implementation.

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

Следующий:

  • 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());
}


  • ConstrainViolationException: это исключение сообщает о результате нарушения ограничений:

@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-компонента с неправильным типом.

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

@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 от клиента

Давайте теперь посмотрим на тест, который запускаетMethodArgumentTypeMismatchException: мы будемsend a request with id as String instead of 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 response:

{
    "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. ОбработатьHttpRequestMethodNotSupportedException

Затем давайте посмотрим на другое интересное исключение -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

Иthe error JSON response:

{
    "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

Иthe error JSON response:

{
    "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. Обработчик по умолчанию

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

@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. Заключение

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

full implementation этого руководства можно найти вthe github project - это проект на основе Eclipse, поэтому его должно быть легко импортировать и запускать как есть.