Estás viendo una versión antigua de esta página. Ve a la versión actual.

Comparar con el actual Ver el historial de la página

« Anterior Versión 21 Siguiente »

Esta guía pretende informar sobre las buenas prácticas a la hora de devolver mensajes de error en APIs REST con FundeWebJS, así como enumerar los mecanismos que proprociona Spring Boot para configurar y manejar estos errores.

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.


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.

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. No debe utilizarse cuando ocurre un error.

  • 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.


Ejemplo ResponseEntity
    //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.

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

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 para validar parámetros.


  1. 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:

    Ejemplo 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).

    Petición:

    Request ResponseStatusException
    https://fundewebjsshowcasedesa.um.es/api/v1.0/public/handleError/5

    Respuesta:

    Response 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 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 ejecución:

    Ejemplo 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;
    }
    ...


    Request con @ResponseStatus
    https://fundewebjsshowcasedesa.um.es/api/v1.0/public/handleError/1
    Respuesta 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:

    Custom 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

    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 sobreescribir esta gestión, lo que 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 y modificar los valores de la respuesta http así como el modelo.
    Desventajas: solo funciona de forma local en el controlador REST donde se haya definido.

    Ejemplo de uso: Para probarlo simplemente se define un POJO con los datos para el body de la respuesta, se crea el método anotado con ExceptionHandler y se lanza la excepción a capturar.

    POJO para definir el modelo de respuesta
    @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";
    
    }
    
    

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

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

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

    Uso 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;
    }
    ...


    Ejemplo llamada:

    URL de ejemplo de petición
    https://fundewebjsshowcasedesa.um.es/api/v1.0/public/handleError/3
    Respuesta
    {
        "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

    Permite centralizar el manejo de excepciones para que se capturen de forma global.

    Poner código de servicio y enlaces a gitlab.
    Poner petición y respuesta ejemplo de postman.


  • ControllerAdvice + ResponseEntityExceptionHandler

    Permite extender y sobreescribir el manejo de las excepciones internas que realiza Spring Boot de forma automática.
    Poner listado de excepciones.

    Poner código de servicio y enlaces a gitlab.
    Poner petición y respuesta ejemplo de postman.


  • Bean Validation

    Configuración y uso de Bean Validation.
    Handlers a sobreescribir para modificar respuesta HTTP o modelo.
    Poner código de servicio y enlaces a gitlab.
    Poner petición y respuesta ejemplo de postman.



Conclusión


Enumerar conclusiones, si se quiere hacer rápido usar ResponseStatusException, si se quiere cambiar la respuesta o el modelo y tenemos muchas excepciones usar ControllerAdvice + ResponseEntityExceptionHandler.

En un futuro cercano se integrará con el RFC Problem, se recomienda no elaborar muchos modelos de respuesta ya que posiblemente deban extender Problem.


Artículos Relacionados


No hay ningún contenido con las etiquetas especificadas



  • Sin etiquetas