Gestion des messages d’erreur personnalisés pour l’API REST

Gestion personnalisée des messages d'erreur pour l'API REST

1. Vue d'ensemble

Dans ce didacticiel, nous expliquerons comment implémenter un gestionnaire d'erreurs global pour une API Spring REST.

Nous utiliserons la sémantique de chaque exception pour créer des messages d'erreur significatifs pour le client, dans le but clair de donner à ce client toutes les informations nécessaires pour diagnostiquer facilement le problème.

Lectures complémentaires:

Spring ResponseStatusException

Apprenez à appliquer des codes d'état aux réponses HTTP dans Spring avec ResponseStatusException.

Read more

Traitement des erreurs pour REST avec Spring

Gestion des exceptions pour une API REST - illustrez la nouvelle approche recommandée par Spring 3.2 ainsi que les solutions précédentes.

Read more

2. Un message d'erreur personnalisé

Commençons par implémenter une structure simple pour l'envoi d'erreurs sur le fil - lesApiError:

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

Les informations ici devraient être simples:

  • status: le code d'état HTTP

  • message: le message d'erreur associé à l'exception

  • error: Liste des messages d'erreur construits

Et bien sûr, pour la logique de gestion des exceptions dans Spring,we’ll useest l'annotation@ControllerAdvice:

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

3. Gérer les exceptions de demande incorrecte

3.1. Gérer les exceptions

Voyons maintenant comment nous pouvons gérer les erreurs client les plus courantes - en gros, des scénarios où un client a envoyé une demande non valide à l'API:

  • BindException: cette exception est levée lorsque des erreurs de liaison fatales se produisent.

  • MethodArgumentNotValidException: cette exception est levée lorsque l'argument annoté avec@Valid a échoué à la validation:

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


Comme vous pouvez le voir,we are overriding a base method out of the ResponseEntityExceptionHandler and providing our own custom implementation.

Ce ne sera pas toujours le cas - nous devrons parfois gérer une exception personnalisée qui n’a pas d’implémentation par défaut dans la classe de base, comme nous le verrons plus tard ici.

Prochain:

  • MissingServletRequestPartException: cette exception est levée lorsque la partie d'une requête en plusieurs parties est introuvable

  • MissingServletRequestParameterException: cette exception est levée lorsque la demande de paramètre manquant:

@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: cette exception signale le résultat des violations de contrainte:

@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: cette exception est levée lorsque vous essayez de définir une propriété de bean avec un type incorrect.

  • MethodArgumentTypeMismatchException: cette exception est levée lorsque l'argument de méthode n'est pas le type attendu:

@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. Consommer l'API du client

Jetons maintenant un coup d'œil à un test a qui s'exécute dans unMethodArgumentTypeMismatchException: nous allonssend 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"));
}

Et enfin - compte tenu de cette même demande:

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

Voici à quoi ressemblera ce type de réponse d'erreur 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. HandleNoHandlerFoundException

Ensuite, nous pouvons personnaliser notre servlet pour qu'il lève cette exception au lieu d'envoyer la réponse 404 - comme suit:


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

Ensuite, une fois que cela se produit, nous pouvons simplement le gérer comme n'importe quelle autre exception:

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


Voici un test simple:

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

Jetons un œil à la demande complète:

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

Et leserror 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. HandleHttpRequestMethodNotSupportedException

Ensuite, jetons un œil à une autre exception intéressante - lesHttpRequestMethodNotSupportedException - qui se produit lorsque vous envoyez une requête avec une méthode HTTP non prise en charge:

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


Voici un test simple reproduisant cette exception:

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

Et voici la demande complète:

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

Etthe 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. HandleHttpMediaTypeNotSupportedException

Maintenant, traitonsHttpMediaTypeNotSupportedException - qui se produit lorsque le client envoie une requête avec un type de support non pris en charge - comme suit:

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


Voici un test simple sur ce problème:

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

Enfin, voici un exemple de demande:

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

Etthe 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. Gestionnaire par défaut

Enfin, implémentons un gestionnaire de secours - un type de logique fourre-tout qui traite toutes les autres exceptions qui n'ont pas de gestionnaires spécifiques:

@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. Conclusion

La création d'un gestionnaire d'erreur adéquat et mature pour une API REST Spring est un processus ardu et certainement itératif. Espérons que ce tutoriel sera un bon point de départ pour le faire pour votre API et un bon point d'ancrage pour aider les clients de votre API à diagnostiquer rapidement et facilement les erreurs et à les dépasser.

Lesfull implementation de ce didacticiel se trouvent dansthe github project - il s'agit d'un projet basé sur Eclipse, il devrait donc être facile à importer et à exécuter tel quel.