Gestion des erreurs pour REST avec Spring

Haut REST

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS

1. Vue d'ensemble

Ce didacticiel illustrera comment implémenter la gestion des exceptions avec Spring pour une API REST. Nous aurons également un aperçu historique et verrons quelles nouvelles options les différentes versions ont introduites.

Avant Spring 3.2, les deux principales approches de gestion des exceptions dans une application Spring MVC étaient HandlerExceptionResolver ou l' annotation @ExceptionHandler . Les deux ont des inconvénients évidents.

Depuis la version 3.2, nous avons l' annotation @ControllerAdvice pour répondre aux limitations des deux solutions précédentes et pour promouvoir une gestion unifiée des exceptions dans toute une application.

Maintenant, Spring 5 introduit la classe ResponseStatusException - un moyen rapide de gérer les erreurs de base dans nos API REST.

Tous ces éléments ont une chose en commun: ils gèrent très bien la séparation des préoccupations . L'application peut lancer des exceptions normalement pour indiquer un échec quelconque, qui sera ensuite traité séparément.

Enfin, nous verrons ce que Spring Boot apporte à la table et comment nous pouvons le configurer en fonction de nos besoins.

2. Solution 1: le contrôleur @ExceptionHandler

La première solution fonctionne au niveau @Controller . Nous allons définir une méthode pour gérer les exceptions et l'annoter avec @ExceptionHandler :

public class FooController{ //... @ExceptionHandler({ CustomException1.class, CustomException2.class }) public void handleException() { // } }

Cette approche présente un inconvénient majeur: T il @ExceptionHandler méthode annotées est actif uniquement pour ce contrôleur particulier , pas globalement pour toute l'application. Bien sûr, l'ajout de ceci à chaque contrôleur le rend mal adapté à un mécanisme général de gestion des exceptions.

Nous pouvons contourner cette limitation en demandant à tous les contrôleurs d'étendre une classe de contrôleur de base.

Cependant, cette solution peut poser problème pour les applications où, pour une raison quelconque, cela n'est pas possible. Par exemple, les contrôleurs peuvent déjà s'étendre à partir d'une autre classe de base, qui peut être dans un autre bocal ou ne pas être directement modifiable, ou peuvent eux-mêmes ne pas être directement modifiables.

Ensuite, nous examinerons une autre façon de résoudre le problème de gestion des exceptions - une méthode globale qui n'inclut aucune modification des artefacts existants tels que les contrôleurs.

3. Solution 2: le HandlerExceptionResolver

La deuxième solution consiste à définir un HandlerExceptionResolver. Cela résoudra toute exception levée par l'application. Cela nous permettra également d'implémenter un mécanisme uniforme de gestion des exceptions dans notre API REST.

Avant d'opter pour un résolveur personnalisé, passons en revue les implémentations existantes.

3.1. ExceptionHandlerExceptionResolver

Ce résolveur a été introduit dans Spring 3.1 et est activé par défaut dans DispatcherServlet . C'est en fait le composant principal du fonctionnement du mécanisme @ ExceptionHandler présenté précédemment.

3.2. DefaultHandlerExceptionResolver

Ce résolveur a été introduit dans Spring 3.0 et il est activé par défaut dans DispatcherServlet .

Il est utilisé pour résoudre les exceptions Spring standard à leurs codes d'état HTTP correspondants, à savoir les codes d'état d'erreur client 4xx et d'erreur serveur 5xx . Voici la liste complète des exceptions Spring qu'il gère et comment elles correspondent aux codes d'état.

Bien qu'il définisse correctement le code d'état de la réponse, une limitation est qu'il ne définit rien dans le corps de la réponse. Et pour une API REST - le code d'état n'est vraiment pas assez d'informations à présenter au client - la réponse doit également avoir un corps, pour permettre à l'application de donner des informations supplémentaires sur l'échec.

Cela peut être résolu en configurant la résolution de la vue et en rendant le contenu d'erreur via ModelAndView , mais la solution n'est clairement pas optimale. C'est pourquoi Spring 3.2 a introduit une meilleure option que nous aborderons dans une section ultérieure.

3.3. ResponseStatusExceptionResolver

Ce résolveur a également été introduit dans Spring 3.0 et est activé par défaut dans DispatcherServlet .

Sa principale responsabilité est d'utiliser l' annotation @ResponseStatus disponible sur les exceptions personnalisées et de mapper ces exceptions aux codes d'état HTTP.

Une telle exception personnalisée peut ressembler à:

@ResponseStatus(value = HttpStatus.NOT_FOUND) public class MyResourceNotFoundException extends RuntimeException { public MyResourceNotFoundException() { super(); } public MyResourceNotFoundException(String message, Throwable cause) { super(message, cause); } public MyResourceNotFoundException(String message) { super(message); } public MyResourceNotFoundException(Throwable cause) { super(cause); } }

Tout comme le DefaultHandlerExceptionResolver , ce résolveur est limité dans la manière dont il traite le corps de la réponse - il mappe le code d'état sur la réponse, mais le corps est toujours nul.

3.4. SimpleMappingExceptionResolver et AnnotationMethodHandlerExceptionResolver

Le SimpleMappingExceptionResolver existe depuis un certain temps. Il provient de l'ancien modèle Spring MVC et n'est pas très pertinent pour un service REST. Nous l'utilisons essentiellement pour mapper les noms de classe d'exception pour afficher les noms.

Le AnnotationMethodHandlerExceptionResolver a été introduit au printemps 3.0 pour gérer les exceptions par l' @ExceptionHandler annotation mais a été désapprouvée par ExceptionHandlerExceptionResolver au printemps 3.2.

3.5. HandlerExceptionResolver personnalisé

La combinaison de DefaultHandlerExceptionResolver et ResponseStatusExceptionResolver contribue grandement à fournir un bon mécanisme de gestion des erreurs pour un service Spring RESTful. L'inconvénient est, comme mentionné précédemment, l' absence de contrôle sur le corps de la réponse.

Idéalement, nous aimerions être en mesure de sortir soit JSON ou XML, selon le format demandé par le client (via l'en- tête Accept ).

Cela justifie à lui seul la création d' un nouveau résolveur d'exceptions personnalisé :

@Component public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver { @Override protected ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { if (ex instanceof IllegalArgumentException) { return handleIllegalArgument( (IllegalArgumentException) ex, response, handler); } ... } catch (Exception handlerException) { logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException); } return null; } private ModelAndView handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response) throws IOException { response.sendError(HttpServletResponse.SC_CONFLICT); String accept = request.getHeader(HttpHeaders.ACCEPT); ... return new ModelAndView(); } }

Un détail à noter ici est que nous avons accès à la requête elle-même, nous pouvons donc considérer la valeur de l'en- tête Accept envoyé par le client.

Par exemple, si le client demande application / json , alors, dans le cas d'une condition d'erreur, nous voudrions nous assurer que nous renvoyons un corps de réponse codé avec application / json .

L'autre détail important de l'implémentation est que nous retournons un ModelAndView - c'est le corps de la réponse , et cela nous permettra de définir tout ce qui est nécessaire dessus.

Cette approche est un mécanisme cohérent et facilement configurable pour la gestion des erreurs d'un service Spring REST.

Cependant, il a des limites: il interagit avec le HtttpServletResponse de bas niveau et s'intègre dans l'ancien modèle MVC qui utilise ModelAndView , il y a donc encore place à l'amélioration.

4. Solution 3: @ControllerAdvice

Spring 3.2 prend en charge un @ExceptionHandler global avec l' annotation @ControllerAdvice .

Cela active un mécanisme qui rompt avec l'ancien modèle MVC et utilise ResponseEntity avec la sécurité de type et la flexibilité de @ExceptionHandler :

@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(value = { IllegalArgumentException.class, IllegalStateException.class }) protected ResponseEntity handleConflict( RuntimeException ex, WebRequest request) { String bodyOfResponse = "This should be application specific"; return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request); } }

The@ControllerAdvice annotation allows us to consolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.

The actual mechanism is extremely simple but also very flexible:

  • It gives us full control over the body of the response as well as the status code.
  • It provides mapping of several exceptions to the same method, to be handled together.
  • It makes good use of the newer RESTful ResposeEntity response.

One thing to keep in mind here is to match the exceptions declared with @ExceptionHandler to the exception used as the argument of the method.

If these don't match, the compiler will not complain — no reason it should — and Spring will not complain either.

However, when the exception is actually thrown at runtime, the exception resolving mechanism will fail with:

java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...] HandlerMethod details: ...

5. Solution 4: ResponseStatusException (Spring 5 and Above)

Spring 5 introduced the ResponseStatusException class.

We can create an instance of it providing an HttpStatus and optionally a reason and a cause:

@GetMapping(value = "/{id}") public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) { try { Foo resourceById = RestPreconditions.checkFound(service.findOne(id)); eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response)); return resourceById; } catch (MyResourceNotFoundException exc) { throw new ResponseStatusException( HttpStatus.NOT_FOUND, "Foo Not Found", exc); } }

What are the benefits of using ResponseStatusException?

  • Excellent for prototyping: We can implement a basic solution quite fast.
  • One type, multiple status codes: One exception type can lead to multiple different responses. This reduces tight coupling compared to the @ExceptionHandler.
  • We won't have to create as many custom exception classes.
  • We have more control over exception handling since the exceptions can be created programmatically.

And what about the tradeoffs?

  • There's no unified way of exception handling: It's more difficult to enforce some application-wide conventions as opposed to @ControllerAdvice, which provides a global approach.
  • Code duplication: We may find ourselves replicating code in multiple controllers.

We should also note that it's possible to combine different approaches within one application.

For example, we can implement a @ControllerAdvice globally but also ResponseStatusExceptions locally.

However, we need to be careful: If the same exception can be handled in multiple ways, we may notice some surprising behavior. A possible convention is to handle one specific kind of exception always in one way.

For more details and further examples, see our tutorial on ResponseStatusException.

6. Handle the Access Denied in Spring Security

The Access Denied occurs when an authenticated user tries to access resources that he doesn't have enough authorities to access.

6.1. MVC — Custom Error Page

First, let's look at the MVC style of the solution and see how to customize an error page for Access Denied.

The XML configuration:

  ...  

And the Java configuration:

@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedPage("/my-error-page"); }

When users try to access a resource without having enough authorities, they will be redirected to “/my-error-page”.

6.2. Custom AccessDeniedHandler

Next, let's see how to write our custom AccessDeniedHandler:

@Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) throws IOException, ServletException { response.sendRedirect("/my-error-page"); } }

And now let's configure it using XML configuration:

  ...  

0r using Java configuration:

@Autowired private CustomAccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedHandler(accessDeniedHandler) }

Note how in our CustomAccessDeniedHandler, we can customize the response as we wish by redirecting or displaying a custom error message.

6.3. REST and Method-Level Security

Finally, let's see how to handle method-level security @PreAuthorize, @PostAuthorize, and @Secure Access Denied.

Of course, we'll use the global exception handling mechanism that we discussed earlier to handle the AccessDeniedException as well:

@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ AccessDeniedException.class }) public ResponseEntity handleAccessDeniedException( Exception ex, WebRequest request) { return new ResponseEntity( "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN); } ... }

7. Spring Boot Support

Spring Boot provides an ErrorController implementation to handle errors in a sensible way.

In a nutshell, it serves a fallback error page for browsers (a.k.a. the Whitelabel Error Page) and a JSON response for RESTful, non-HTML requests:

{ "timestamp": "2019-01-17T16:12:45.977+0000", "status": 500, "error": "Internal Server Error", "message": "Error processing the request!", "path": "/my-endpoint-with-exceptions" }

As usual, Spring Boot allows configuring these features with properties:

  • server.error.whitelabel.enabled: can be used to disable the Whitelabel Error Page and rely on the servlet container to provide an HTML error message
  • server.error.include-stacktrace: with an always value; includes the stacktrace in both the HTML and the JSON default response

Apart from these properties, we can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.

We can also customize the attributes that we want to show in the response by including an ErrorAttributes bean in the context. We can extend the DefaultErrorAttributes class provided by Spring Boot to make things easier:

@Component public class MyCustomErrorAttributes extends DefaultErrorAttributes { @Override public Map getErrorAttributes( WebRequest webRequest, boolean includeStackTrace) { Map errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); errorAttributes.put("locale", webRequest.getLocale() .toString()); errorAttributes.remove("error"); //... return errorAttributes; } }

If we want to go further and define (or override) how the application will handle errors for a particular content type, we can register an ErrorController bean.

Again, we can make use of the default BasicErrorController provided by Spring Boot to help us out.

Par exemple, imaginons que nous souhaitons personnaliser la façon dont notre application gère les erreurs déclenchées dans les points de terminaison XML. Tout ce que nous avons à faire est de définir une méthode publique à l'aide de @RequestMapping , et de déclarer qu'elle produit le type de support application / xml :

@Component public class MyErrorController extends BasicErrorController { public MyErrorController(ErrorAttributes errorAttributes) { super(errorAttributes, new ErrorProperties()); } @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE) public ResponseEntity xmlError(HttpServletRequest request) { // ... } }

8. Conclusion

Cet article décrit plusieurs façons d'implémenter un mécanisme de gestion des exceptions pour une API REST dans Spring, en commençant par l'ancien mécanisme et en continuant avec le support Spring 3.2 et dans 4.x et 5.x.

Comme toujours, le code présenté dans cet article est disponible à l'adresse over sur GitHub.

Pour le code lié à Spring Security, vous pouvez vérifier le module spring-security-rest.

REST bas

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS