Versiones comparadas

Clave

  • Se ha añadido esta línea.
  • Se ha eliminado esta línea.
  • El formato se ha cambiado.

...


Tabla de contenidos
maxLevel4

...

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
languagejava
themeEclipse
titleEjemplo ResponseEntity
collapsetrue
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. 
Por defecto, si no se pasan parámetros en la declaración de la notación @RestControllerAdvice, los manejadores serán comunes a toda la aplicación.

...

);


...

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. 
Por defecto, si no se pasan parámetros en la declaración de la notación @RestControllerAdvice, los manejadores serán comunes a toda la aplicación.

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
languagejava
themeEclipse
titleExcepción a capturar
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
languagejava
themeEclipse
titleExcepción a capturar
@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
languagejava
themeEclipse
titleExcepción a capturar
@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
languagejson
titleRespuesta JSON
{
    "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
languagejava
themeEclipse
titleEjemplo ResponseEntity
collapsetrue
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:

  1. ResponseStatusException => Devolver modelo de respuesta estándar de forma rápida y correcta .
  2. DefaultErrorAttributes => Modificar el modelo de respuesta de forma global.
  3. ExceptionHandler => Manejar localmente una excepción propia y devolver el modelo de respuesta estándar o un modelo propio.
  4. ControllerAdvice => Manejar de forma global todas mis excepciones y devolver el modelo de respuesta estándar o un modelo propio.
  5. ControllerAdvice + ResponseEntityExceptionHandler => Modificar el comportamiento y el modelo de respuesta para excepciones internas capturadas por Spring Boot.
  6. [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
languagejson
titleRespuesta JSON
{
    "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).

...