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.
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
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
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.DispatcherServletthrowExceptionIfNoHandlerFoundtrue
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());
}
{
"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:
{
"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.