...
| Tabla de contenidos | ||
|---|---|---|
|
...
Buenas Prácticas
Las principales indicaciones a la hora de construir una respuesta en un servicio REST (tanto para éxito como para error) son:
Usar la clase ResponseEntity<T> ( org.springframework.http.ResponseEntity) para envolver la respuesta.
Usar los códigos de estado de HTTP para indicar el resultado de la petición.
Este es el subconjunto de códigos de estado aconsejado para cada situación:
200 - Sólo utilizar en caso de RESPUESTA CORRECTA en peticiones GET. No debe utilizarse cuando ocurre un error
- 201 - Sólo utilizar en caso de RESPUESTA CORRECTA en peticiones POST y PUT.
- 204 - Para respuestas a peticiones PUT, PATCH y DELETE correctas que no devuelven ningún body. (Ver documentación Spring)
400 - Mensaje mal formado, petición o url incorrecta.
401 - Fallo de autenticación.
403 - Acceso no permitido a un recurso.
404 - Recurso no encontrado.
405 - Método HTTP (GET, POST, PUT…) no permitido.
406 - MediaType solicitado no soportado (Accept en la petición y Produces del servicio no concuerdan).
415 - ContentType enviado no soportado (ContentType en la petición y Consumes del servicio no concuerdan).
500 - Error general en el servidor.
503 - Servidor no disponible temporalmente.
La clase ResponseEntity proporciona varios métodos estáticos y builders para construir la respuesta en nuestros endpoints en base a estos códigos de forma muy rápida.
Es aconsejable usar estos códigos de estado HTTP mediante el enumerado org.springframework.http.HttpStatus.
| Bloque de código | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
private ResponseEntity< ProblemDetailDto> buildErrorResponse( HttpStatus status, Exception ex, HttpServletRequest req ) { return ResponseEntity .status(statusHttpStatus.NOT_FOUND) .body( ProblemDetailDto .builder() .status( status ) .fecha(LocalDateTime.now()) .error( ex.getMessagebuild() ).exception( ex.getClass().getName() ).path( req.getRequestURI() ).method( req.getMethod() ).principal( req.getUserPrincipal().getName() ) .build()); } @ExceptionHandler(NoSuchElementException.class) public ResponseEntity<ProblemDetailDto> handleNoSuchElement(Exception ex, HttpServletRequest req) { log.error("NoSuchElementException - User: {}. Method: {}. Exception: {}", req.getUserPrincipal().getName(), ex.getClass().getName(), ex.getMessage()); return buildErrorResponse( HttpStatus.NOT_FOUND, ex, req ); } |
Guía detallada
...
ControllerAdvice
@ControllerAdvice permite interceptar y manejar excepciones de forma global, sin necesidad de duplicar lógica en cada controlador.
Objetivos
Centralizar la gestión de errores.
Garantizar un modelo de respuesta uniforme.
Manejar excepciones propias y de terceros.
Características
Se aplica a todos los controladores.
Permite devolver cualquier estructura de respuesta (JSON estándar, modelo propio, etc.).
Facilita la trazabilidad y el mantenimiento.
...
| Info |
|---|
Es posible declarar varios RestControllerAdvice para dividir el manejo de excepciones por paquetes, clases, anotaciones, etc. |
...
);
|
...
Guía detallada
Spring Boot ofrece varios mecanismos para manejar excepciones y controlar las respuestas de error. Aunque todos son válidos, el enfoque recomendado es centralizar la gestión de errores mediante @ControllerAdvice. A partir de este mecanismo principal, el resto se entienden como herramientas complementarias.
ControllerAdvice
@ControllerAdvice permite interceptar y manejar excepciones de forma global, sin necesidad de duplicar lógica en cada controlador.
Objetivos
Centralizar la gestión de errores.
Garantizar un modelo de respuesta uniforme.
Manejar excepciones propias y de terceros.
Características
Se aplica a todos los controladores.
Permite devolver cualquier estructura de respuesta (JSON estándar, modelo propio, etc.).
Facilita la trazabilidad y el mantenimiento.
La gestión centralizada y global de todas las excepciones en nuestra aplicación se consigue utilizando la anotación @RestControllerAdvice, que actúa como un punto único para interceptar y manejar cualquier error que se produzca en los controladores.
| Info |
|---|
Es posible declarar varios RestControllerAdvice para dividir el manejo de excepciones por paquetes, clases, anotaciones, etc. |
Para manejar una excepción mediante @RestControllerAdvice, se identifica la excepción que se desea capturar —ya sea una excepción personalizada o una excepción ya existente en el sistema— y se define un POJO que representará el modelo de respuesta siguiendo el protocolo de errores RFC 9421. A continuación, se implementa una clase anotada con @RestControllerAdvice que contiene un método @ExceptionHandler encargado de interceptar dicha excepción y generar la respuesta estructurada correspondiente.
Ejemplo de uso:
| Bloque de código | ||||||
|---|---|---|---|---|---|---|
| ||||||
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.net.URI;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProblemDetailDto {
@NotNull
private URI type; // URI que identifica el tipo de error
@NotBlank
private String title; // Título corto del error
@NotNull
private Integer status; // Código HTTP
@NotBlank
private String detail; // Descripción detallada
@NotNull
private URI instance; // URI de la petición
@Valid
private List<InvalidParam> invalidParams;
} |
| Bloque de código | ||||||
|---|---|---|---|---|---|---|
| ||||||
@RestControllerAdvice( basePackageClasses = {
CertificadosRest.class
} )
public class RestControllerAdviceError {
private ResponseEntity<ProblemDetailDto> buildErrorResponse( HttpStatus status, Exception ex, HttpServletRequest req ) {
return ResponseEntity
.status(status)
.body(ErrorDTO.builder()
.status( status )
.fecha(LocalDateTime.now())
.error( ex.getMessage() ).exception( | ||||||
| Bloque de código | ||||||
| ||||||
@RestControllerAdvice( basePackageClasses = { CertificadosRest.class } ) public class RestControllerAdviceError { private ResponseEntity<ProblemDetailDto> buildErrorResponse( HttpStatus status, Exception ex, HttpServletRequest req ) { return ResponseEntity .status(status) .body(ErrorDTO.builder() .status( status ) .fecha(LocalDateTime.now()) .error( ex.getMessage() ).exception( ex.getClass().getName() ).path( req.getRequestURI() ).method( req.getMethod() ).principal( req.getUserPrincipal().getName() ) .build()); } @ExceptionHandler(NoSuchElementException.class) public ResponseEntity<ProblemDetailDto> handleNoSuchElement(Exception ex, HttpServletRequest req) { log.error("NoSuchElementException - User: {}. Method: {}. Exception: {}", req.getUserPrincipal().getName(), ex.getClass().getName(), ex.getMessage()); return buildErrorResponse( HttpStatus.NOT_FOUND, ex, req ); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity<ProblemDetailDto> handleIllegalArgumentException(Exception ex, HttpServletRequest req) { log.error("IllegalArgumentException - User: {}. Method: {}. Exception: {}", req.getUserPrincipal().getName(), ex.getClass().getName(), ex.getMessage()); return buildErrorResponse( HttpStatus.BAD_REQUEST, ex, req ); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ProblemDetailDto> handleDatosBodyInvalidos(Exception ex, HttpServletRequest req) { log.error("MethodArgumentNotValidException - User: {}. Method: {}. Exception: {}", req.getUserPrincipal().getName(), ex.getClass().getName(), ex).getMessagepath( req.getRequestURI()); return buildErrorResponse( HttpStatus.BAD_REQUEST, ex, req ) ).method( req.getMethod() ).principal( req.getUserPrincipal().getName() ) .build()); } @ExceptionHandler( { AuthorizationServiceException.class, ServicioPersonaNoEncontrada.class, TokenExpiredException.class, UnauthorizedException.class } NoSuchElementException.class) public ResponseEntity<ProblemDetailDto> handleAuthorizationServiceExceptionhandleNoSuchElement(Exception ex, HttpServletRequest req) { log.error("AuthorizationServiceExceptionNoSuchElementException - User: {}. Method: {}. Exception: {}", req.getUserPrincipal().getName(), ex.getClass().getName(), ex.getMessage()); return buildErrorResponse( HttpStatus.UNAUTHORIZEDNOT_FOUND, ex, req ); } @ExceptionHandler( WebClientResponseExceptionIllegalArgumentException.class ) public ResponseEntity<ProblemDetailDto> WebClientResponseExceptionhandleIllegalArgumentException( Exception ex, HttpServletRequest req ) { log.error( "AuthorizationServiceExceptionIllegalArgumentException - User: {}. Method: {}. Exception: {}", req.getUserPrincipal().getName(), ex.getClass().getName(), ex.getMessage() ); return buildErrorResponse( HttpStatus.UNAUTHORIZEDBAD_REQUEST, ex, req ); } @ExceptionHandler(AccessDeniedExceptionMethodArgumentNotValidException.class) public ResponseEntity<ProblemDetailDto> handleAccessDeniedExceptionhandleDatosBodyInvalidos(Exception ex, HttpServletRequest req) { log.error("AccessDeniedExceptionMethodArgumentNotValidException - User: {}. Method: {}. Exception: {}", req.getUserPrincipal().getName(), ex.getClass().getName(), ex.getMessage()); return buildErrorResponse( HttpStatus.FORBIDDENBAD_REQUEST, ex, req ); } @ExceptionHandler( ServicioCertificadosException.class { AuthorizationServiceException.class, ServicioPersonaNoEncontrada.class, TokenExpiredException.class, UnauthorizedException.class } ) public ResponseEntity<ProblemDetailDto> handleGeneralExceptionhandleAuthorizationServiceException(Exception ex, HttpServletRequest req) { log.error("ExceptionAuthorizationServiceException - User: {}. Method: {}. Exception: {}", req.getUserPrincipal().getName(), ex.getClass().getName(), ex.getMessage()); return buildErrorResponse( HttpStatus.INTERNAL_SERVER_ERRORUNAUTHORIZED, ex, req ); } } |
Ejemplo de petición:
| Bloque de código | ||||
|---|---|---|---|---|
| ||||
{
"estado": "INTERNAL_SERVER_ERROR",
"fecha": "05/10/2021 01:37:25",
"mensaje": "4. Excepción con ControllerAdvice y modelo custom TestControllerAdviceError",
"tipoRespuesta": "CUSTOM_ControllerAdvice",
"erroresValidacion": null
} |
Spring Boot proporciona varios mecanismos para capturar excepciones (global o localmente) y devolver mensajes de error (estándar o personalizados). Estos mecanismos han ido evolucionando con las diversas versiones del framework y, en algunos casos, pueden llegar a solaparse.
| Bloque de código | ||||||||
|---|---|---|---|---|---|---|---|---|
| ||||||||
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.net.URI;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProblemDetailDto {
@NotNull
private URI type; // URI que identifica el tipo de error
@NotBlank
private String title; // Título corto del error
@NotNull
private Integer status; // Código HTTP
@NotBlank
private String detail; // Descripción detallada
@NotNull
private URI instance; // URI de la petición
@Valid
private List<InvalidParam> invalidParams;
} |
A continuación se va a explicar estos mecanismos desde el enfoque de los casos de uso más habituales:
ResponseStatusException => Devolver modelo de respuesta estándar de forma rápida y correcta .
DefaultErrorAttributes => Modificar el modelo de respuesta de forma global.
ExceptionHandler => Manejar localmente una excepción propia y devolver el modelo de respuesta estándar o un modelo propio.
ControllerAdvice => Manejar de forma global todas mis excepciones y devolver el modelo de respuesta estándar o un modelo propio.
ControllerAdvice + ResponseEntityExceptionHandler => Modificar el comportamiento y el modelo de respuesta para excepciones internas capturadas por Spring Boot.
[EXTRA] Bean Validation => Modificar la respuesta si se usa este estándar para la validación de los parámetros.
...
@ExceptionHandler( WebClientResponseException.class )
public ResponseEntity<ProblemDetailDto> WebClientResponseException( Exception ex, HttpServletRequest req ) {
log.error( "AuthorizationServiceException - User: {}. Method: {}. Exception: {}", req.getUserPrincipal().getName(), ex.getClass().getName(), ex.getMessage() );
return buildErrorResponse( HttpStatus.UNAUTHORIZED, ex, req );
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ProblemDetailDto> handleAccessDeniedException(Exception ex, HttpServletRequest req) {
log.error("AccessDeniedException - User: {}. Method: {}. Exception: {}", req.getUserPrincipal().getName(), ex.getClass().getName(), ex.getMessage());
return buildErrorResponse( HttpStatus.FORBIDDEN, ex, req );
}
@ExceptionHandler( ServicioCertificadosException.class )
public ResponseEntity<ProblemDetailDto> handleGeneralException(Exception ex, HttpServletRequest req) {
log.error("Exception - User: {}. Method: {}.", req.getUserPrincipal().getName(), ex.getClass().getName(), ex);
return buildErrorResponse( HttpStatus.INTERNAL_SERVER_ERROR, ex, req );
}
} |
Ejemplo de petición:
| Bloque de código | ||||
|---|---|---|---|---|
| ||||
{
"estado": "INTERNAL_SERVER_ERROR",
"fecha": "05/10/2021 01:37:25",
"mensaje": "4. Excepción con ControllerAdvice y modelo custom TestControllerAdviceError",
"tipoRespuesta": "CUSTOM_ControllerAdvice",
"erroresValidacion": null
} |
Otros mecanismos
ResponseStatusException
Útil para devolver un error de forma correcta y rápida, sin preocuparnos de los datos devueltos.
Se construye la excepción pasándole el status code HTTP de error deseado y Spring Boot la captura y maneja de forma automática.
Devuelve el modelo de respuesta estándar (ver siguiente apartado).
...