Guía para el manejo de errores de la API REST de Spring Boot

Publicado: 2022-03-11

Manejar correctamente los errores en las API y proporcionar mensajes de error significativos es una característica muy deseable, ya que puede ayudar al cliente de la API a responder adecuadamente a los problemas. El comportamiento predeterminado tiende a devolver seguimientos de pila que son difíciles de entender y, en última instancia, inútiles para el cliente API. La partición de la información de error en campos también permite que el cliente de la API la analice y proporcione mejores mensajes de error al usuario. En este artículo, cubriremos cómo realizar un manejo de errores adecuado al crear una API REST con Spring Boot.

Persona confundida por un mensaje de error críptico y largo

La creación de API REST con Spring se convirtió en el enfoque estándar para los desarrolladores de Java durante los últimos años. El uso de Spring Boot ayuda sustancialmente, ya que elimina una gran cantidad de código repetitivo y permite la configuración automática de varios componentes. Asumiremos que está familiarizado con los conceptos básicos del desarrollo de API con esas tecnologías antes de aplicar los conocimientos que se describen aquí. Si aún no está seguro de cómo desarrollar una API REST básica, debe comenzar con este artículo sobre Spring MVC u otro sobre la creación de un servicio Spring REST.

Hacer que las respuestas de error sean más claras

A lo largo de este artículo, usaremos el código fuente alojado en GitHub de una aplicación que implementa una API REST para recuperar objetos que representan aves. Tiene las características descritas en este artículo y algunos ejemplos más de escenarios de manejo de errores. Aquí hay un resumen de los puntos finales implementados en esa aplicación:

</tr>
GET /birds/{birdId} Obtiene información sobre un pájaro y lanza una excepción si no se encuentra.
GET /birds/noexception/{birdId} Esta llamada también obtiene información sobre un pájaro, excepto que no lanza una excepción en caso de que no se encuentre el pájaro.
POST /birds Crea un pájaro.

El módulo Spring Framework MVC viene con algunas características excelentes para ayudar con el manejo de errores. Pero se deja al desarrollador usar esas funciones para tratar las excepciones y devolver respuestas significativas al cliente API.

Veamos un ejemplo de la respuesta predeterminada de Spring Boot cuando emitimos un HTTP POST al punto final /birds con el siguiente objeto JSON, que tiene la cadena "aaa" en el campo "masa", que debería estar esperando un número entero:

 { "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }

La respuesta predeterminada de Spring Boot, sin el manejo adecuado de errores:

 { "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]", "path": "/birds" }

Bueno… el mensaje de respuesta tiene algunos buenos campos, pero se enfoca demasiado en cuál fue la excepción. Por cierto, esta es la clase DefaultErrorAttributes de Spring Boot. El campo de timestamp de tiempo es un número entero que ni siquiera contiene información sobre la unidad de medida en la que se encuentra la marca de tiempo. El campo de exception solo es interesante para los desarrolladores de Java y el mensaje deja al consumidor de la API perdido en todos los detalles de implementación que son irrelevantes para ellos. . ¿Y si hubiera más detalles que pudiéramos extraer de la excepción de la que se originó el error? Entonces, aprendamos cómo tratar esas excepciones correctamente y envolverlas en una representación JSON más agradable para hacer la vida más fácil para nuestros clientes de API.

Como usaremos las clases de fecha y hora de Java 8, primero debemos agregar una dependencia de Maven para los convertidores Jackson JSR310. Se encargan de convertir las clases de fecha y hora de Java 8 a una representación JSON utilizando la anotación @JsonFormat :

 <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>

Bien, definamos una clase para representar los errores de la API. Crearemos una clase llamada ApiError que tenga suficientes campos para contener información relevante sobre los errores que ocurren durante las llamadas REST.

 class ApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List<ApiSubError> subErrors; private ApiError() { timestamp = LocalDateTime.now(); } ApiError(HttpStatus status) { this(); this.status = status; } ApiError(HttpStatus status, Throwable ex) { this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } ApiError(HttpStatus status, String message, Throwable ex) { this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); } }
  • La propiedad de status contiene el estado de la llamada de operación. Será cualquier cosa, desde 4xx para indicar errores del cliente o 5xx para indicar errores del servidor. Un escenario común es un código http 400 que significa BAD_REQUEST, cuando el cliente, por ejemplo, envía un campo con formato incorrecto, como una dirección de correo electrónico no válida.

  • La propiedad timestamp contiene la instancia de fecha y hora de cuando ocurrió el error.

  • La propiedad del message contiene un mensaje fácil de usar sobre el error.

  • La propiedad debugMessage contiene un mensaje del sistema que describe el error con más detalle.

  • La propiedad subErrors contiene una matriz de suberrores que ocurrieron. Esto se usa para representar múltiples errores en una sola llamada. Un ejemplo serían los errores de validación en los que varios campos han fallado en la validación. La clase ApiSubError se usa para encapsularlos.

 abstract class ApiSubError { } @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor class ApiValidationError extends ApiSubError { private String object; private String field; private Object rejectedValue; private String message; ApiValidationError(String object, String message) { this.object = object; this.message = message; } }

Entonces, ApiValidationError es una clase que extiende ApiSubError y expresa los problemas de validación encontrados durante la llamada REST.

A continuación, verá algunos ejemplos de respuestas JSON que se generan después de implementar las mejoras descritas aquí, solo para tener una idea de lo que tendremos al final de este artículo.

Aquí hay un ejemplo de JSON devuelto cuando no se encuentra una entidad al llamar al punto final GET /birds/2 :

 { "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }

Aquí hay otro ejemplo de JSON devuelto al emitir una llamada POST /birds con un valor no válido para la masa del pájaro:

 { "apierror": { "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [ { "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000" } ] } }

Manejo de errores de arranque de primavera

Exploremos algunas de las anotaciones de Spring que se usarán para manejar las excepciones.

RestController es la anotación base para las clases que manejan operaciones REST.

ExceptionHandler es una anotación de Spring que proporciona un mecanismo para tratar las excepciones que se generan durante la ejecución de los controladores (operaciones del controlador). Esta anotación, si se usa en métodos de clases de controlador, servirá como punto de entrada para manejar excepciones lanzadas solo dentro de este controlador. En conjunto, la forma más común es usar @ExceptionHandler en los métodos de las clases @ControllerAdvice para que el manejo de excepciones se aplique globalmente o a un subconjunto de controladores.

ControllerAdvice es una anotación introducida en Spring 3.2 y, como sugiere el nombre, es "Consejo" para varios controladores. Se utiliza para permitir que un solo ExceptionHandler se aplique a varios controladores. De esta forma, podemos definir en un solo lugar cómo tratar dicha excepción y se llamará a este controlador cuando se produzca la excepción desde las clases cubiertas por este ControllerAdvice . El subconjunto de controladores afectados se puede definir mediante los siguientes selectores en @ControllerAdvice : annotations() , basePackageClasses() y basePackages() . Si no se proporcionan selectores, ControllerAdvice se aplica globalmente a todos los controladores.

Entonces, al usar @ExceptionHandler y @ControllerAdvice , podremos definir un punto central para tratar las excepciones y envolverlas en un objeto ApiError con una mejor organización que el mecanismo predeterminado de manejo de errores de Spring Boot.

Manejo de excepciones

Representación de lo que sucede con una llamada de cliente REST exitosa y fallida

El siguiente paso es crear la clase que manejará las excepciones. Para simplificar, lo RestExceptionHandler y debe extenderse desde ResponseEntityExceptionHandler de Spring Boot. Ampliaremos ResponseEntityExceptionHandler , ya que proporciona un manejo básico de las excepciones de Spring MVC, por lo que agregaremos controladores para nuevas excepciones y mejoraremos las existentes.

Anulación de excepciones manejadas en ResponseEntityExceptionHandler

Si observa el código fuente de ResponseEntityExceptionHandler , verá muchos métodos llamados handle******() como handleHttpMessageNotReadable() o handleHttpMessageNotWritable() . Primero veamos cómo podemos extender handleHttpMessageNotReadable() para manejar las excepciones HttpMessageNotReadableException . Solo tenemos que anular el método handleHttpMessageNotReadable() en nuestra clase RestExceptionHandler :

 @Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request"; return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); } private ResponseEntity<Object> buildResponseEntity(ApiError apiError) { return new ResponseEntity<>(apiError, apiError.getStatus()); } //other exception handlers below }

Hemos declarado que en caso de que se lance una HttpMessageNotReadableException , el mensaje de error será "Solicitud JSON con formato incorrecto" y el error se encapsulará dentro del objeto ApiError . A continuación podemos ver la respuesta de una llamada REST con este nuevo método anulado:

 { "apierror": { "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]" } }

Manejo de excepciones personalizadas

Ahora veremos cómo crear un método que maneje una excepción que aún no se ha declarado dentro de ResponseEntityExceptionHandler de Spring Boot.

Un escenario común para una aplicación Spring que maneja llamadas a bases de datos es tener una llamada para buscar un registro por su ID usando una clase de repositorio. Pero si observamos el método CrudRepository.findOne() , veremos que devuelve un null si no se encuentra un objeto. Eso significa que si nuestro servicio solo llama a este método y regresa directamente al controlador, obtendremos un código HTTP 200 (OK) incluso si no se encuentra el recurso. De hecho, el enfoque correcto es devolver un código HTTP 404 (NO ENCONTRADO) como se especifica en la especificación HTTP/1.1.

Para manejar este caso, crearemos una excepción personalizada llamada EntityNotFoundException . Esta es una excepción creada a medida y diferente de javax.persistence.EntityNotFoundException , ya que proporciona algunos constructores que facilitan la creación de objetos, y uno puede elegir manejar la excepción javax.persistence de manera diferente.

Ejemplo de una llamada REST fallida

Dicho esto, vamos a crear un ExceptionHandler para esta EntityNotFoundException recién creada en nuestra clase RestExceptionHandler . Para hacer eso, cree un método llamado handleEntityNotFound() y anótelo con @ExceptionHandler , pasándole el objeto de clase EntityNotFoundException.class . Esto le indica a Spring que cada vez que se lanza una EntityNotFoundException , Spring debe llamar a este método para manejarlo. Al anotar un método con @ExceptionHandler , aceptará una amplia gama de parámetros autoinyectados como WebRequest , Locale y otros, como se describe aquí. Solo proporcionaremos la excepción EntityNotFoundException como parámetro para este método handleEntityNotFound .

 @Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<Object> handleEntityNotFound( EntityNotFoundException ex) { ApiError apiError = new ApiError(NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); } }

¡Genial! En el método handleEntityNotFound() , estamos configurando el código de estado HTTP en NOT_FOUND y usando el nuevo mensaje de excepción. Así es como se ve ahora la respuesta para el punto final GET /birds/2 :

 { "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }

Conclusión

Es importante tener el control del manejo de excepciones para que podamos asignar correctamente esas excepciones al objeto ApiError y proporcionar información importante que permita a los clientes de la API saber qué sucedió. El siguiente paso a partir de aquí sería crear más métodos de controlador (los que tienen @ExceptionHandler) para las excepciones que se generan dentro del código de la aplicación. Hay más ejemplos de algunas otras excepciones comunes como MethodArgumentTypeMismatchException , ConstraintViolationException y otras en el código de GitHub.

Aquí hay algunos recursos adicionales que ayudaron en la redacción de este artículo:

  • Baeldung - Manejo de errores para REST con Spring

  • Spring Blog - Manejo de excepciones en Spring MVC