Manejo de errores para REST con Spring

DESCANSO Arriba

Acabo de anunciar el nuevo curso Learn Spring , centrado en los fundamentos de Spring 5 y Spring Boot 2:

>> VER EL CURSO

1. Información general

Este tutorial ilustrará cómo implementar el manejo de excepciones con Spring para una API REST. También obtendremos una descripción general histórica y veremos qué nuevas opciones introdujeron las diferentes versiones.

Antes de Spring 3.2, los dos enfoques principales para manejar excepciones en una aplicación Spring MVC eran HandlerExceptionResolver o la anotación @ExceptionHandler . Ambos tienen algunas desventajas claras.

Desde la versión 3.2, hemos tenido la anotación @ControllerAdvice para abordar las limitaciones de las dos soluciones anteriores y promover un manejo unificado de excepciones en toda la aplicación.

Ahora Spring 5 presenta la clase ResponseStatusException , una forma rápida para el manejo básico de errores en nuestras API REST.

Todos ellos tienen algo en común: abordan muy bien la separación de preocupaciones . La aplicación puede generar excepciones normalmente para indicar una falla de algún tipo, que luego se manejará por separado.

Finalmente, veremos qué aporta Spring Boot y cómo podemos configurarlo para que se adapte a nuestras necesidades.

2. Solución 1: el controlador @ExceptionHandler

La primera solución funciona a nivel de @Controller . Definiremos un método para manejar excepciones y lo anotaremos con @ExceptionHandler :

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

Este enfoque tiene un inconveniente importante: T él @ExceptionHandler método anotado sólo está activa para ese controlador particular, y no mundial para toda la aplicación. Por supuesto, agregar esto a cada controlador hace que no sea adecuado para un mecanismo general de manejo de excepciones.

Podemos solucionar esta limitación haciendo que todos los controladores amplíen una clase de controlador base.

Sin embargo, esta solución puede ser un problema para aplicaciones donde, por cualquier motivo, eso no es posible. Por ejemplo, los controladores ya pueden extenderse desde otra clase base, que puede estar en otro frasco o no modificarse directamente, o pueden no ser directamente modificables.

A continuación, veremos otra forma de resolver el problema de manejo de excepciones: una que es global y no incluye ningún cambio en los artefactos existentes, como los controladores.

3. Solución 2: HandlerExceptionResolver

La segunda solución es definir un HandlerExceptionResolver. Esto resolverá cualquier excepción lanzada por la aplicación. También nos permitirá implementar un mecanismo uniforme de manejo de excepciones en nuestra API REST.

Antes de optar por un solucionador personalizado, repasemos las implementaciones existentes.

3.1. ExceptionHandlerExceptionResolver

Esta resolución se introdujo en Spring 3.1 y está habilitada de forma predeterminada en DispatcherServlet . Este es en realidad el componente central de cómo funciona el mecanismo @ ExceptionHandler presentado anteriormente.

3.2. DefaultHandlerExceptionResolver

Esta resolución se introdujo en Spring 3.0 y está habilitada de forma predeterminada en DispatcherServlet .

Se utiliza para resolver las excepciones estándar de Spring a sus códigos de estado HTTP correspondientes, a saber, los códigos de estado de error de cliente 4xx y error de servidor 5xx . Aquí está la lista completa de las Excepciones de Spring que maneja y cómo se asignan a los códigos de estado.

Si bien establece el código de estado de la respuesta correctamente, una limitación es que no establece nada en el cuerpo de la respuesta. Y para una API REST, el código de estado en realidad no es información suficiente para presentar al cliente, la respuesta también debe tener un cuerpo, para permitir que la aplicación brinde información adicional sobre la falla.

Esto se puede resolver configurando la resolución de la vista y renderizando el contenido del error a través de ModelAndView , pero la solución claramente no es óptima. Es por eso que Spring 3.2 introdujo una mejor opción que discutiremos en una sección posterior.

3.3. ResponseStatusExceptionResolver

Este resolutor también se introdujo en Spring 3.0 y está habilitado por defecto en DispatcherServlet .

Su principal responsabilidad es utilizar la anotación @ResponseStatus disponible en excepciones personalizadas y asignar estas excepciones a los códigos de estado HTTP.

Esta excepción personalizada puede verse así:

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

Al igual que DefaultHandlerExceptionResolver , este solucionador está limitado en la forma en que trata el cuerpo de la respuesta: asigna el código de estado en la respuesta, pero el cuerpo sigue siendo nulo.

3.4. SimpleMappingExceptionResolver y AnnotationMethodHandlerExceptionResolver

El SimpleMappingExceptionResolver existe desde hace bastante tiempo. Viene del modelo Spring MVC anterior y no es muy relevante para un servicio REST. Básicamente lo usamos para mapear nombres de clases de excepción para ver nombres.

El AnnotationMethodHandlerExceptionResolver se introdujo en Spring 3.0 para manejar excepciones a través de la anotación @ExceptionHandler, pero ExceptionHandlerExceptionResolver lo ha desaprobado a partir de Spring 3.2.

3.5. HandlerExceptionResolver personalizado

La combinación de DefaultHandlerExceptionResolver y ResponseStatusExceptionResolver contribuye en gran medida a proporcionar un buen mecanismo de manejo de errores para un servicio RESTful de Spring. La desventaja es, como se mencionó anteriormente, no hay control sobre el cuerpo de la respuesta.

Idealmente, nos gustaría poder generar JSON o XML, según el formato que haya solicitado el cliente (a través del encabezado Accept ).

Esto solo justifica la creación de un nuevo solucionador de excepciones personalizado :

@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 detalle a notar aquí es que tenemos acceso a la solicitud en sí, por lo que podemos considerar el valor del encabezado Accept enviado por el cliente.

Por ejemplo, si el cliente solicita application / json , entonces, en el caso de una condición de error, queremos asegurarnos de devolver un cuerpo de respuesta codificado con application / json .

El otro detalle importante de la implementación es que devolvemos un ModelAndView : este es el cuerpo de la respuesta y nos permitirá establecer lo que sea necesario en él.

Este enfoque es un mecanismo consistente y fácilmente configurable para el manejo de errores de un servicio REST de Spring.

Sin embargo, tiene limitaciones: interactúa con HtttpServletResponse de bajo nivel y encaja en el antiguo modelo MVC que usa ModelAndView , por lo que todavía hay margen de mejora.

4. Solución 3: @ControllerAdvice

Spring 3.2 brinda soporte para un @ExceptionHandler global con la anotación @ControllerAdvice .

Esto habilita un mecanismo que rompe con el modelo MVC anterior y hace uso de ResponseEntity junto con la seguridad de tipos y la flexibilidad 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); } }

La anotación @ControllerAdvice nos permite consolidar nuestros múltiples @ExceptionHandler s dispersos de antes en un solo componente global de manejo de errores.

El mecanismo real es extremadamente simple pero también muy flexible:

  • Nos da un control total sobre el cuerpo de la respuesta y el código de estado.
  • Proporciona un mapeo de varias excepciones al mismo método, para ser manejadas juntas.
  • Hace un buen uso de la respuesta RESTful ResposeEntity más reciente .

Una cosa a tener en cuenta aquí es hacer coincidir las excepciones declaradas con @ExceptionHandler con la excepción utilizada como argumento del método.

Si estos no coinciden, el compilador no se quejará, sin razón alguna, y Spring tampoco se quejará.

Sin embargo, cuando la excepción se lanza en tiempo de ejecución, el mecanismo de resolución de excepciones fallará con :

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

5. Solución 4: ResponseStatusException (Spring 5 y superior)

Spring 5 introdujo la clase ResponseStatusException .

Podemos crear una instancia del mismo proporcionando un HttpStatus y, opcionalmente, un motivo y una causa :

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

¿Cuáles son los beneficios de usar ResponseStatusException ?

  • Excelente para la creación de prototipos: podemos implementar una solución básica bastante rápido.
  • Un tipo, múltiples códigos de estado: un tipo de excepción puede dar lugar a múltiples respuestas diferentes. Esto reduce el acoplamiento estrecho en comparación con @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.

Por ejemplo, imagine que queremos personalizar la forma en que nuestra aplicación maneja los errores desencadenados en puntos finales XML. Todo lo que tenemos que hacer es definir un método público usando @RequestMapping , y declarar que produce el tipo de medio 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. Conclusión

Este artículo discutió varias formas de implementar un mecanismo de manejo de excepciones para una API REST en Spring, comenzando con el mecanismo anterior y continuando con el soporte de Spring 3.2 y en 4.xy 5.x.

Como siempre, el código presentado en este artículo está disponible en GitHub.

Para el código relacionado con Spring Security, puede consultar el módulo spring-security-rest.

DESCANSO inferior

Acabo de anunciar el nuevo curso Learn Spring , centrado en los fundamentos de Spring 5 y Spring Boot 2:

>> VER EL CURSO