Versiones comparadas

Clave

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

...

Info

Esta página es la equivalente FundeWebJS a la Wiki Manejo de errores en servicios REST de FundeWeb. 


La integración con el estándar RFC 7807 Problem Details for HTTP APIs, no se incluye por defecto en Spring Boot pero se encuentra en proceso de desarrollo por parte de MNCS.

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.

Info

No es necesario conocer ni utilizar todos los códigos de estado HTTP.

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 -

...

400 - Mensaje mal formado, petición o url incorrecta.

...

    • 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
    //EJEMPLO ResponseEntity con éxito
	public ResponseEntity<String> simpleAsyncTask( @RequestParam( defaultValue = "60" ) long t,
			@RequestParam( defaultValue = "1" ) long sleep ) throws InterruptedException {

		String result = "";
		log.info( "Entro en simpleAsyncTask {} - {}", t, sleep );

		final CompletableFuture<String> asyncStatus = service.asyncMethod( sleep * 1000, t );
		result = asyncStatus.getNow( "ko" );
		log.info( "Salgo de simpleAsyncTask" );

		return ResponseEntity.ok( result );
	}

    //EJEMPLO ResponseEntity con error
	@ExceptionHandler( TestLocalHandlerException.class )
	public ResponseEntity<TestLocalHandlerError> handleTestLocalHandler( TestLocalHandlerException ex ) {

		final TestLocalHandlerError error = TestLocalHandlerError.builder()
				.estado( HttpStatus.INTERNAL_SERVER_ERROR ).fecha( LocalDateTime.now() )
				.mensaje( ex.getMessage() ).build();

		return ResponseEntity.status( HttpStatus.INTERNAL_SERVER_ERROR ).body( error );

	}

Guía detallada  

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.

Info

El término "modelo de respuesta" empleado en esta guía se refiere al formato de JSON (los campos) devuelto cuando sucede una situación de error.

Advertencia

La integración con el estándar  RFC 7807 Problem con Spring Boot está en desarrollo. Cuando esté terminada dicha integración se modificará el modelo de respuesta estándar para contener los campos del RFC Problem (ver wiki manejo de errores en FundeWeb).

Ante cualquier duda póngase en contacto con MNCS.

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.

1. ResponseStatusException

...

Ejemplo de uso:  Se captura una excepción en nuestro controlador Rest y se lanza una nueva ResponseStatusException con los valores deseados. 

Bloque de código
languagejava
themeEclipse
titleEjemplo de uso de ResponseStatusException
try{
.....
throw new TestException( HttpStatus.BAD_REQUEST,"2. ResponseStatusException con FundeWebJSErrorAttributtes" );
.....

} catch ( final TestException e2 ) {
    //Se captura una excepción existente y se lanza una nueva ResponseStatusException
	log.error( "Test de prueba de ResponseStatusException con custom FundeWebJSErrorAttributtes: {}",e2.getMessage() );
	throw new ResponseStatusException( HttpStatus.INTERNAL_SERVER_ERROR, e2.getMessage(),e2.getCause() );

} ....

...

	.status(HttpStatus.NOT_FOUND)
		.body( ProblemDetailDto.builder()
		.status( status )
		.fecha(LocalDateTime.now())
		.build());


...

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( 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.getMessage());
		return buildErrorResponse( HttpStatus.BAD_REQUEST, ex, req );
	}

	@ExceptionHandler( {
		AuthorizationServiceException.class, ServicioPersonaNoEncontrada.class, TokenExpiredException.class, UnauthorizedException.class
	} )
	public ResponseEntity<ProblemDetailDto> handleAuthorizationServiceException(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( 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).


Ventajas: rápido, status code parametrizable.
Desventajas: Modelo de respuesta fijo.

Ejemplo de uso:  Se captura una excepción en nuestro controlador Rest y se lanza una nueva ResponseStatusException con los valores deseados. 

Bloque de código
languagejava
themeEclipse
titleEjemplo de uso de ResponseStatusException
try{
.....
throw new TestException( HttpStatus.BAD_REQUEST,"2. ResponseStatusException con FundeWebJSErrorAttributtes" );
.....

} catch ( final TestException e2 ) {
    //Se captura una excepción existente y se lanza una nueva ResponseStatusException
	log.error( "Test de prueba de ResponseStatusException con custom FundeWebJSErrorAttributtes: {}",e2.getMessage() );
	throw new ResponseStatusException( HttpStatus.INTERNAL_SERVER_ERROR, e2.getMessage(),e2.getCause() );

} ....

FuenteRest handleError (https://gitlab.um.es/mncs/fundewebjsshowcase-api/-/blob/desarrollo/src/main/java/es/um/atica/fundewebjsshowcasebackend/services/rest/impl/RestTestImpl.java).

Advertencia

Se recomienda usar Postman para realizar todas las pruebas de peticiones con FundeWebJS.

Petición:

Bloque de código
languagexml
titleRequest ResponseStatusException
https://fundewebjsshowcasedesa.um.es/api/v1.0/public/handleError/2

Respuesta:

Bloque de código
languagejson
titleResponse ResponseStatusException
{
    "fecha": "05/10/2021 10:31:27",
    "estado": "INTERNAL_SERVER_ERROR",
    "mensaje": "2. ResponseStatusException con FundeWebJSErrorAttributtes",
    "tipoRespuesta": "CUSTOM_DefaultErrorAttributes"
}


1.1 ResponseStatus:

La excepción ResponseStatusException es una evolución de otro mecanismo, la notación @ResponseStatus .
Por comodidad se recomienda usar ResponseStatusException, pero es posible seguir usando esta notación declarándola en nuestras excepciones y lanzándolas desde nuestra API REST directamente. 
Ver: Ejemplo ResponseStatus (https://gitlab.um.es/mncs/fundewebjsshowcase-api/-/blob/desarrollo/src/main/java/es/um/atica/fundewebjsshowcasebackend/exceptions/TestResponseStatusException.java).

Ejemplo de uso:
 Se anota nuestra excepción con ResponseStatus y se lanza desde nuestro controlador Rest.

Bloque de código
languagejava
themeEclipse
titleEjemplo declaración y uso @ResponseStatus
...
@ResponseStatus( code = HttpStatus.BAD_REQUEST, reason = "Reason en ResponseStatus", value = HttpStatus.INTERNAL_SERVER_ERROR )
public class TestResponseStatusException extends RuntimeException
....
/
try{
...   
	throw new TestResponseStatusException( HttpStatus.BAD_REQUEST, "1. Notación ResponseStatus con FundeWebJSErrorAttributtes" );
...
} catch ( final TestResponseStatusException e1 ) {
	log.error( "Test de prueba de notación @ResponseStatus con custom FundeWebJSErrorAttributtes: {}",e1.getMessage() );
	throw e1;
}
...

 

Advertencia

Se recomienda usar Postman para realizar todas las pruebas de peticiones con FundeWebJS.


Bloque de código
titleRequest con @ResponseStatus
https://fundewebjsshowcasedesa.um.es/api/v1.0/public/handleError/1


Bloque de código
languagejson
titleRespuesta con @ResponseStatus
{
    "fecha": "05/10/2021 10:45:25",
    "estado": "BAD_REQUEST",
    "mensaje": "1. Notación ResponseStatus con FundeWebJSErrorAttributtes",
    "tipoRespuesta": "CUSTOM_DefaultErrorAttributes"
}


...

2. DefaultErrorAttributes

Por defecto Spring usa el modelo de respuesta (los campos que se devuelven) devuelto por la clase DefaultErrorAttributes.
Podemos modificar el modelo de respuesta estándar extendiendo esta clase (y anotándola con @Component).

Cambiar el modelo de respuesta implica que, si no se usa otro mecanismo, el nuevo modelo será devuelto en todas las situaciones de error.

Ejemplo de fichero extendido: Se crea una clase nueva que extiende DefaultErrorAttributes, se anota con Component y se sobreescribe el método getErrorAttributes como se desee

Advertencia

Se recomienda usar Postman para realizar todas las pruebas de peticiones con FundeWebJS.

Petición:

Bloque de código
languagexml
titleRequest ResponseStatusException
https://fundewebjsshowcasedesa.um.es/api/v1.0/public/handleError/2

Respuesta:

Bloque de código
languagejson
titleResponse ResponseStatusException
{
    "fecha": "05/10/2021 10:31:27",
    "estado": "INTERNAL_SERVER_ERROR",
    "mensaje": "2. ResponseStatusException con FundeWebJSErrorAttributtes",
    "tipoRespuesta": "CUSTOM_DefaultErrorAttributes"
}

1.1 ResponseStatus:

...

Bloque de código
languagejava
themeEclipse
titleEjemplo declaración y uso @ResponseStatus
...
@ResponseStatus( code = HttpStatus.BAD_REQUEST, reason = "Reason en ResponseStatus", value = HttpStatus.INTERNAL_SERVER_ERROR )
public class TestResponseStatusException extends RuntimeException
....
/
try{
...   
	throw new TestResponseStatusException( HttpStatus.BAD_REQUEST, "1. Notación ResponseStatus con FundeWebJSErrorAttributtes" );
...
} catch ( final TestResponseStatusException e1 ) {
	log.error( "Test de prueba de notación @ResponseStatus con custom FundeWebJSErrorAttributtes: {}",e1.getMessage() );
	throw e1;
}
...
Advertencia

Se recomienda usar Postman para realizar todas las pruebas de peticiones con FundeWebJS.

Bloque de código
titleRequest con @ResponseStatus
https://fundewebjsshowcasedesa.um.es/api/v1.0/public/handleError/1
Bloque de código
languagejson
titleRespuesta con @ResponseStatus
{
    "fecha": "05/10/2021 10:45:25",
    "estado": "BAD_REQUEST",
    "mensaje": "1. Notación ResponseStatus con FundeWebJSErrorAttributtes",
    "tipoRespuesta": "CUSTOM_DefaultErrorAttributes"
}

2. DefaultErrorAttributes

Por defecto Spring usa el modelo de respuesta (los campos que se devuelven) devuelto por la clase DefaultErrorAttributes.
Podemos modificar el modelo de respuesta estándar extendiendo esta clase (y anotándola con @Component).

Cambiar el modelo de respuesta implica que, si no se usa otro mecanismo, el nuevo modelo será devuelto en todas las situaciones de error.

Ejemplo de fichero extendido: Se crea una clase nueva que extiende DefaultErrorAttributes, se anota con Component y se sobreescribe el método getErrorAttributes como se desee.

Bloque de código
languagejava
themeEclipse
titleCustom DefaultErrorAttributes
@Component
public class FundeWebJSErrorAttributes extends DefaultErrorAttributes {

	private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager
			.getLogger( FundeWebJSErrorAttributes.class );

	public static final String TIPO_RESPUESTA = "CUSTOM_DefaultErrorAttributes";

	@Override
	public Map<String, Object> getErrorAttributes( WebRequest webRequest, boolean includeStackTrace ) {
		final Map<String, Object> allErrorAttributes = super.getErrorAttributes( webRequest, includeStackTrace );
		final Map<String, Object> errorAttributes = new HashMap<>();

		final int statusCode = ( int ) allErrorAttributes.get( "status" );
		errorAttributes.put( "tipoRespuesta", TIPO_RESPUESTA );
		errorAttributes.put( "estado", HttpStatus.valueOf( statusCode ) );
		errorAttributes.put( "fecha",
				LocalDateTime.now().format( DateTimeFormatter.ofPattern( "dd/MM/yyyy HH:mm:ss" ) ) );

		String mensaje = "";

		final Throwable throwable = getError( webRequest );

		if ( throwable instanceof ResponseStatusException ) {
			final ResponseStatusException responseStatusException = ( ResponseStatusException ) throwable;
			mensaje = responseStatusException.getReason() == null ? "" : responseStatusException.getReason();

		} else if ( throwable instanceof ConstraintViolationException ) {
			webRequest.setAttribute( "status", HttpStatus.BAD_REQUEST.value(), 1 );
			errorAttributes.put( "estado", HttpStatus.BAD_REQUEST.toString() );
			mensaje = throwable.getMessage() == null ? "" : throwable.getMessage();
		} else {

			if ( ( throwable != null ) && ( throwable.getMessage() != null ) ) {
				mensaje = throwable.getMessage() == null ? "" : throwable.getMessage();
			}

		}

		errorAttributes.put( "mensaje", mensaje );

		return errorAttributes;
	}

}

Fuente: FundeWebJSErrorAttributes.java

Advertencia

No se recomienda desarrollar modelos propios de respuesta estándar hasta que se complete la integración con el RFC 7807 Problem Details for HTTP APIs por parte de MNCS.

3. ExceptionHandler

En los métodos anteriores se ha explicado cómo lanzar excepciones en nuestros servicios y modificar el modelo de respuesta.
A partir de este punto se explicará cómo maneja Spring las excepciones y cómo modificar este mecanismo.
Esto permitirá gestionar las respuestas, definir modelos de respuesta diferentes dependiendo de cada excepción, y sobreescribir el comportamiento estándar en situaciones de error no controladas.

Como primera aproximación a este enfoque tenemos la notación @ExceptionHandler

...

.

Bloque de código
languagejava
themeEclipse
titlePOJO para definir el modelo de respuestaCustom DefaultErrorAttributes
@Component
@Data
@RequiredArgsConstructor
@AllArgsConstructor
@Builder
public class TestLocalHandlerError {
	
	@NonNull
	private HttpStatus estado;
	@JsonFormat(shape = Shape.STRING, pattern = "dd/MM/yyyy hh:mm:ss")
	private LocalDateTime fecha;
	@NonNull
	private String mensaje;
	@Builder.Default
	private String tipoRespuesta = "CUSTOM_LocalHandler";

}

...

Bloque de código
languagejava
themeEclipse
titleExceptionHandler
@ExceptionHandler( TestLocalHandlerException.class )
public ResponseEntity<TestLocalHandlerError> handleTestLocalHandler( TestLocalHandlerException ex ) {

	final TestLocalHandlerError error = TestLocalHandlerError.builder()
			.estado( HttpStatus.INTERNAL_SERVER_ERROR ).fecha( LocalDateTime.now() )
			.mensaje( ex.getMessage() ).build();

	return ResponseEntity.status( HttpStatus.INTERNAL_SERVER_ERROR ).body( error );
}

...

FundeWebJSErrorAttributes extends DefaultErrorAttributes {

	private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager
			.getLogger( FundeWebJSErrorAttributes.class );

	public static final String TIPO_RESPUESTA = "CUSTOM_DefaultErrorAttributes";

	@Override
	public Map<String, Object> getErrorAttributes( WebRequest webRequest, boolean includeStackTrace ) {
		final Map<String, Object> allErrorAttributes = super.getErrorAttributes( webRequest, includeStackTrace );
		final Map<String, Object> errorAttributes = new HashMap<>();

		final int statusCode = ( int ) allErrorAttributes.get( "status" );
		errorAttributes.put( "tipoRespuesta", TIPO_RESPUESTA );
		errorAttributes.put( "estado", HttpStatus.valueOf( statusCode ) );
		errorAttributes.put( "fecha",
				LocalDateTime.now().format( DateTimeFormatter.ofPattern( "dd/MM/yyyy HH:mm:ss" ) ) );

		String mensaje = "";

		final Throwable throwable = getError( webRequest );

		if ( throwable instanceof ResponseStatusException ) {
			final ResponseStatusException responseStatusException = ( ResponseStatusException ) throwable;
			mensaje = responseStatusException.getReason() == null ? "" : responseStatusException.getReason();

		} else if ( throwable instanceof ConstraintViolationException ) {
			webRequest.setAttribute( "status", HttpStatus.BAD_REQUEST.value(), 1 );
			errorAttributes.put( "estado", HttpStatus.BAD_REQUEST.toString() );
			mensaje = throwable.getMessage() == null ? "" : throwable.getMessage();
		} else {

			if ( ( throwable != null ) && ( throwable.getMessage() != null ) ) {
				mensaje = throwable.getMessage() == null ? "" : throwable.getMessage();
			}

		}

		errorAttributes.put( "mensaje", mensaje );

		return errorAttributes;
	}

}

Fuente: FundeWebJSErrorAttributes.java

Advertencia

No se recomienda desarrollar modelos propios de respuesta estándar hasta que se complete la integración con el RFC 7807 Problem Details for HTTP APIs por parte de MNCS.


...

3. ExceptionHandler

En los métodos anteriores se ha explicado cómo lanzar excepciones en nuestros servicios y modificar el modelo de respuesta.
A partir de este punto se explicará cómo maneja Spring las excepciones y cómo modificar este mecanismo.
Esto permitirá gestionar las respuestas, definir modelos de respuesta diferentes dependiendo de cada excepción, y sobreescribir el comportamiento estándar en situaciones de error no controladas.

Como primera aproximación a este enfoque tenemos la notación @ExceptionHandler

Ventajas:  Permite manejar una excepción, modificando la respuesta http así como el modelo.
Desventajas: solo funciona de forma local en el controlador REST donde se haya definido.

Ejemplo de uso: Se define un POJO con los datos para el body de la respuesta; en nuestro controlador Rest se crea  método anotado con ExceptionHandler; y se lanza la excepción a manejar.

Bloque de código
languagejava
themeEclipse
titleUso en servicio REST
...
try{
   throw new TestLocalHandlerException("3. Excepción con handler local y modelo custom TestLocalHandlerError" );
} catch ( final TestLocalHandlerException e3 ) {
   log.error( "Test de prueba de excepción con handler en el propio controller y modelo TestLocalHandlerError: {}", e3.getMessage() );
   throw e3;
}
...

...

Advertencia

Se recomienda usar Postman para realizar todas las pruebas de peticiones con FundeWebJS.

Bloque de código
titleURL de ejemplo de petición
https://fundewebjsshowcasedesa.um.es/api/v1.0/public/handleError/3
Bloque de código
languagejson
titleRespuesta
{
    "estado": "INTERNAL_SERVER_ERROR",
    "fecha": "05/10/2021 12:37:46",
    "mensaje": "3. Excepción con handler local y modelo custom TestLocalHandlerError",
    "tipoRespuesta": "CUSTOM_LocalHandler"
}

4. ControllerAdvice

El principal inconveniente del mecanismo anterior es que, si queremos manejar la misma excepción en varios controllers, habría que escribir el mismo método en cada uno de ellos.

...

Info

La notación RestControllerAdvice engloba las notaciones ControllerAdvice (que registra la clase en el contexto de Spring para que pueda acceder a todos los controllers) y ResponseBody (para que los pojos enviados en la respuesta se interpreten como JSON).

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.

...

Bloque de código
languagejava
themeEclipse
titleExcepción a capturar
public class TestControllerAdviceException extends RuntimeException {

	private static final long serialVersionUID = 1L;

	public TestControllerAdviceException( String msg ) {
		super( msg );
	}
}

Fuente: TestControllerAdviceException.java (https://gitlab.um.es/mncs/fundewebjsshowcase-api/-/blob/desarrollo/src/main/java/es/um/atica/fundewebjsshowcasebackend/exceptions/TestControllerAdviceException.java)

Bloque de código
languagejava
themeEclipse
titlePOJO con para definir el modelo de respuesta de error
@Data
@RequiredArgsConstructor
@AllArgsConstructor
@Builder
public class TestControllerAdviceErrorTestLocalHandlerError {
	
	@Nonnull@NonNull
	private HttpStatus estado;
	@JsonFormat(shape = Shape.STRING, pattern = "dd/MM/yyyy hh:mm:ss")
	private LocalDateTime fecha;
	@Nonnull@NonNull
	private String mensaje;
	@Builder.Default
	private String tipoRespuesta = "CUSTOM_ControllerAdviceLocalHandler";
    private List<String> erroresValidacion;
}


FuenteTestControllerAdviceErrorTestLocalHandlerError.java  (https://gitlab.um.es/mncs/fundewebjsshowcase-api/-/blob/desarrollo/src/main/java/es/um/atica/fundewebjsshowcasebackend/error/TestControllerAdviceErrorTestLocalHandlerError.java)

Bloque de código
languagejava
themeEclipse
titleClase RestControllerAdvice
Eclipse
titleExceptionHandler
@ExceptionHandler( TestLocalHandlerException@RestControllerAdvice( basePackages = "es.um.atica.fundewebjsshowcasebackend" )
public class TestControllerAdvice {

	/**
	 * Handler de TestControllerAdviceException
	 *
	 * @param ex
	 * @return
	 * @see TestControllerAdviceException
	 * @see RestTestImpl
	 */
	@ExceptionHandler( TestControllerAdviceException.class )
	public ResponseEntity<TestControllerAdviceError>ResponseEntity<TestLocalHandlerError> handleTestControllerAdvicehandleTestLocalHandler( TestControllerAdviceExceptionTestLocalHandlerException ex ) {

		final TestControllerAdviceErrorTestLocalHandlerError error = TestControllerAdviceErrorTestLocalHandlerError.builder()
				.estado( HttpStatus.INTERNAL_SERVER_ERROR ).fecha( LocalDateTime.now() )
			.mensaje( ex.getMessage() )
				.build();

		return ResponseEntity.status( HttpStatus.INTERNAL_SERVER_ERROR ).body( error );
	}}

Fuente: TestControllerAdvice.java Rest Controller Test (https://gitlab.um.es/mncs/fundewebjsshowcase-api/-/blob/desarrollo/src/main/java/es/um/atica/fundewebjsshowcasebackend/handler/TestControllerAdviceservices/rest/impl/RestTestImpl.java)

Bloque de código
languagejava
themeEclipse
titleLanzar excepción Uso en servicio REST
try{
...
throwtry{
   throw new TestControllerAdviceExceptionTestLocalHandlerException("43. Excepción con ControllerAdvicehandler local y modelo custom TestControllerAdviceErrorTestLocalHandlerError" );
...
} catch ( final TestControllerAdviceExceptionTestLocalHandlerException e4e3 ) {
	log   log.error( "Test de prueba de excepción con handler en el ControllerAdvicepropio controller y modelo TestControllerAdviceErrorTestLocalHandlerError: {}", e4.getMessage() );
	throw e4;
}

...

 e3.getMessage() );
   throw e3;
}
...


Ejemplo de petición:

Advertencia

Se recomienda usar Postman para realizar todas las pruebas de peticiones con FundeWebJS.

...

Bloque de código
titleURL de ejemplo de petición
https://fundewebjsshowcasedesa.um.es/api/v1.0/public/handleError/43


Bloque de código
languagejson
titleRespuesta JSON
{
    "estado": "INTERNAL_SERVER_ERROR",
    "fecha": "05/10/2021 0112:37:2546",
    "mensaje": "43. Excepción con ControllerAdvicehandler local y modelo custom TestControllerAdviceErrorTestLocalHandlerError",
    "tipoRespuesta": "CUSTOM_ControllerAdviceLocalHandler",
    "erroresValidacion": null
}

...




...

5. ControllerAdvice + ResponseEntityExceptionHandler

Según la documentación de Spring, existe una clase interna que proporciona un manejo centralizado de todas las excepciones en métodos anotados con @RequestMapping a través de la notación @ExceptionHandler. Se trata de la clase ResponseEntityExceptionHandler.
Es decir, si queremos usar nuestro ControllerAdvice declarado en el ejemplo anterior, y ampliarlo para modificar las respuestas en toda tipo de excepciones (internas o propias), solo tenemos que extender la clase ResponseEntityExceptionHandler.

...

Fuente: TestControllerAdvice.java (https://gitlab.um.es/mncs/fundewebjsshowcase-api/-/blob/desarrollo/src/main/java/es/um/atica/fundewebjsshowcasebackend/handler/TestControllerAdvice.java)

...

6. Bean Validation

Si estamos utilizando el estándar Bean Validation para realizar la validación de parámetros (path o request), posiblemente queramos capturar la excepción lanzada automáticamente para cambiar el mensaje (para incluir los errores de validación) y el código de respuesta HTTP (para devolver un 400 Bad Request en lugar de un 500).

...

Info

La configuración y uso de Bean Validation queda como objetivo de la página de confluence: [EN CONSTRUCCIÓN]. Ante cualquier duda contacte con MNCS o revise las fuentes del proyecto de ejemplo fundewebjsshowcase-api.



...

Conclusión

...

Como resumen de lo expuesto en esta página podemos hacer el siguiente listado de casos de uso:

...

Info

Próximamente estará disponible la integración con el RFC Problem, por lo que no se recomienda elaborar muchos modelos de respuesta propios.

Estará disponible un modelo de respuesta común para las aplicaciones FundeWebJS y se indicará cómo manejar las excepciones de forma más ágil.



Artículos Relacionados

...