Manejo de errores en servicios REST

Esta Wiki pretende dar respuesta a las preguntas:

  • ¿Qué devuelvo en mi servicio REST cuando ocurre un error controlado?
  • ¿Qué objeto debe devolver?¿Qué MediaType debe tener?
  • ¿Y si es un problema de validación del método o los parámetros?
  • ¿Existe un mecanismo rápido y común en ÁTICA para generar la respuesta en estos casos?

Para resolver estas cuestiones esta WiKi describe una serie de directrices para crear la respuesta de un servicio REST cuando sucede alguna situación de error (un recurso no encontrado, acceso a un método no autorizado…).

Además, con el fin de estandarizar las respuestas de error devueltas por todos los grupos de ATICA, se ha implementado una solución basada en:

Las principales indicaciones a la hora de construir una respuesta en un servicio REST (tanto para éxito como para error) son:

  • Usar la clase Response (javax.ws.rs.core.Response).
  • 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.

Las clases Response y ResponseBuilder proporcionan varios métodos estáticos para crear una instancia de Response en base a estos códigos de forma muy rápida.

Es aconsejable usar estos códigos de estado HTTP mediante el enumerado Response.Status.

En los últimos años se ha popularizado el uso de la especificación RFC 7807 para definir un formato común en las respuestas de error para APIs HTTP.


Podemos describir un objeto Problem como un JSON que contiene las siguientes propiedades:

  • type - (String) URI que especifica el problema. Si los diferentes tipos de error han sido identificados y publicados, esta propiedad debe contener la URI de publicación donde se describe el problema. Esta propiedad siempre debe estar rellenada. En caso de no existir el valor por defecto es “about:blank”.
  • title - (String) Resumen del tipo de problema.
  • status - (Number) Código de estado HTTP correspondiente al problema.
  • detail - (String) Descripción del problema.
  • instance - (String) URI (o path) que especifica la petición que ocasionó el problema.


Ejemplo:

  {
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    "balance": 30,
    "accounts": ["/account/12345",
                 "/account/67890"]
   }


Además, la especificación define dos nuevos MediaTypes que serán devueltos en la cabecera de la respuesta dependiendo del tipo aceptado en la petición:

  • application/problem+json.
  • application/problem+xml.


Una vez puestos en conocimiento tanto la especificación Problem como el mapeo de excepciones y la validación de métodos, a continuación se describe el mecanismo incluido en las librerías de FundeWeb para implementar el manejo de errores de forma rápida y estandarizada.

Disponible solo para aplicaciones FundeWeb 1.5 migradas a Weblogic 12.2, FundWeb 2.0 y versiones posteriores.

Para el IDE FundeWeb 2.1 está disponible a partir de la siguiente versión de las librerias:

  • fundeweb-jaxrs-2.0.101.
  • fundeweb-jersey-2.0.102.
  • fundeweb-jaxrs-1.5.101.

Para el IDE FundeWeb 2.0 está disponible a partir de la siguiente versión de las librerias:

  • fundeweb-jaxrs-2.0.4.
  • fundeweb-jersey-2.0.14.

Configuración

Este paso solo es necesario en aplicaciones FundeWeb 1.5 migradas a Weblogic 12.2. Tenemos que abrir el fichero cxf-beans.xml y añadimos los siguientes beans al final del fichero:

    <bean id="genericExceptionMapper" class="javax.ws.rs.ext.GenericExceptionMapper"/>
    <bean id="genericThrowableMapper" class="javax.ws.rs.ext.GenericThrowableMapper"/>
    <bean id="webApplicationExceptionMapper" class="javax.ws.rs.ext.WebApplicationExceptionMapper"/>
    <bean id="problemMessageBodyWriter" class="org.umu.atica.jaxrs.problem.writers.ProblemMessageBodyWriter"/>
    <bean id="problemContainerRequestFilter" class="org.umu.atica.jaxrs.problem.ProblemContainerRequestFilter"/>
    <bean id="problemContainerResponseFilter" class="org.umu.atica.jaxrs.problem.ProblemContainerResponseFilter"/>

Para poder utilizarlos, tenemos que añadirlos a un elemento <jaxrs:server> o <jaxrs:client>.

    <jaxrs:server id="servicioRest" address="/rest">
        <jaxrs:serviceBeans>
            <ref bean="serviceBean" /> <!-- Un servicio Rest -->
        </jaxrs:serviceBeans>
        <jaxrs:providers>
            <ref bean="genericExceptionMapper"/>
            <ref bean="genericThrowableMapper"/>
            <ref bean="webApplicationExceptionMapper"/>
            <ref bean="problemContainerRequestFilter"/>
            <ref bean="problemContainerResponseFilter"/>
            <ref bean="problemMessageBodyWriter"/>
        </jaxrs:providers>
    </jaxrs:server>

Hay que tener encuenta que problemContainerRequestFilter tiene que ser el primer ContainerRequestFilter declarado y que problemContainerResponseFilter tiene que ser el último ContainerResponseFilter declarado.

Clases


GenericProblem

Clase que implementa la interfaz Problem para representar la especificación RFC 7807. Además extiende Throwable, por lo que será posible lanzar una instancia directamente o mediante una excepción.

La clase GenericProblem contiene varios métodos estáticos para construir una instancia rápidamente a partir de alguno de los atributos principales:

    public static Problem fromType( URI type ) 
    public static Problem fromStatus( StatusType status ) 
    public static Problem fromTypeStatus( URI type, StatusType status ) 
    public static Problem fromStatusDetail( StatusType status, String detail ) 

Se encuentra en la librería fundeweb-jaxrs.


WebApplicationException

Excepción incluida en JAX-RS para proporcionar una forma sencilla y apropiada de crear excepciones en servicios REST.
Es la raíz de toda la jerarquía de excepciones añadidas en JAX-RS 2.0 que nos permitirá lanzar excepciones adecuadas a cada situación de error:

Excepción Código estado HTTP Uso
BadRequestException 400 Mensaje mal formado
NotAuthorizedException 401 Fallo de autenticación
ForbiddenException 403 Acceso no permitido
NotFoundException 404 Recurso no encontrado
NotAllowedException 405 Método HTTP no soportado
NotAcceptableException 406 MediaType solicitado no soportado
NotSupportedException 415 MediaType enviado (POST/PUT) no soportado
InternalServerErrorException 500 Error general en el servidor
ServiceUnavailableException 503 Servicio temporalmente no disponible


Todas estas excepciones también están incluidas en la librería fundeweb-jaxrs, y las podemos utilizar para lanzar respuestas de error automáticas.


Modo de Empleo

Una vez comentado los tipos de objetos que intervienen en el manejo de errores, a continuación se explican las diferentes situaciones y modos de empleo:

Para los ejemplos se mostrará siempre la respuesta en formato JSON.
Aunque sea el formato recomendado, la respuesta de error se devolverá con el MediaType solicitado por el cliente.

1. Excepción no controlada

Cuando ocurra una excepción no controlada en nuestro servicio la respuesta siempre contendrá de forma automática un Problem genérico.

Ejemplo:

	@GET
	@Path( "/personas/{id}" )
	@Produces( MediaType.APPLICATION_JSON )
	public Response getPersonaPrueba(@PathParam( "id" ) String documento ) {
 
		PersonaPrueba pp = null;
		pp.setNombre( "NullPointer de Jesús" );
		pp.setApellido( "Exception Sánchez" );
 
		return Response.ok( pp ).build();
	}

Respuesta:

 HTTP/1.1 500 Internal Server Error
 Content-Type: application/problem+json
 
{
    "type": "about:blank",
    "status": 500,
    "title": "Internal Server Error",
    "detail": "java.lang.NullPointerException",
    "parameters": {}
}


2. Errores de Validación

Solo para aplicaciones FundeWeb 2.0 y versiones posteriores.

Si se está utilizando la Validación de Servicios REST y un método/parámetro no es válido, la respuesta contendrá siempre de forma automática un Problem que incluirá los errores de validación.
El código de estado HTTP devuelto será el establecido en la WiKi de Validación.


Ejemplo:

	@GET
	@Path( "/personas/{id}" )
	@Produces( MediaType.APPLICATION_JSON )
	public Response getPersonaPrueba( @NotNull @HeaderParam( "user" ) String user,
			@Email @PathParam( "id" ) String documento ) {
 
		// Da igual, aquí no va a entrar por que va a fallar la validación
 
		return Response.ok().build();
	}

Petición:
Se realiza la llamada al servicio sin incluir “user” en la cabecera y con “documento” sin formato de email para que fallen las validaciones @NotNull e @Email respectivamente:

/personas/1

Respuesta:

 HTTP/1.1 400 Bad Request
 Content-Type: application/problem+json
 
{
    "type": "about:blank",
    "status": 400,
    "title": "Bad Request",
    "detail": "javax.validation.ConstraintViolationException",
    "parameters": {},
    "validationErrors": [
        {
            "message": "no es una dirección de correo bien formada",
            "messageTemplate": "{org.hibernate.validator.constraints.Email.message}",
            "path": "PruebasRESTImpl#getPersonaPrueba(arg1)",
            "invalidValue": "1"
        },
        {
            "message": "no puede ser null",
            "messageTemplate": "{javax.validation.constraints.NotNull.message}",
            "path": "PruebasRESTImpl#getPersonaPrueba(arg0)",
            "invalidValue": null
        }
    ]
}

Esta situación ocurre de forma automática al producirse un error de validación.
Solo se menciona a título informativo. No es necesario programar nada.

3. Excepción básica

Si queremos informar de un error controlado (siguiendo las indicaciones del apartado “Buenas prácticas” de esta WiKi), lo más sencillo es lanzar la excepción de JAX-RS correspondiente al error producido.


Ejemplo:

        @GET
	@Path( "/personas/{id}" )
	@Produces( MediaType.APPLICATION_JSON )
	public Response getPersonaPrueba( @Email @PathParam( "id" ) String documento ) {
 
		PersonaPrueba pp = new PersonaPrueba();
		Persona personaBuscada = null;
 
		try {
			personaBuscada = servicioGenteUmu
					.getPersonaByUsuario( EnvironmentManagerBean.instance().getApplicationName(), documento );
 
			pp.setNombre( personaBuscada.getNombre() );
			pp.setApellido( personaBuscada.getApellidos() );
		} catch ( PersonaException | PersonaNotFoundException e ) {
			log.error( "Se ha producido un error al buscar el usuario [#0].", e, documento );
			throw new NotFoundException(e);
		}
 
		return Response.ok( pp ).build();
	}

Petición:

En este ejemplo, se realiza la petición con un correo inexistente para que el ServicioGenteUmu no lo encuentre y podamos lanzar la excepción JAX-RS NotFoundException.

/personas/persona@um.es


Respuesta:

 HTTP/1.1 404 Not Found
 Content-Type: application/problem+json
 
{
    "type": "about:blank",
    "status": 404,
    "title": "Not Found",
    "detail": "org.umu.atica.servicios.gesper.gente.exceptions.PersonaNotFoundException: No se encuentra la persona: No se encontro persona con usuario persona@um.es",
    "parameters": {}
}


Otros ejemplos:
Debido a que GenericProblem extiende Throwable existen varias formas de lanzar las excepciones, aunque el resultado es prácticamente idéntico al que acabamos de ver.

     ....
     //Ejemplo 2
     } catch ( PersonaException | PersonaNotFoundException e ) {
	log.error( "Se ha producido un error al buscar el usuario [#0].", e, documento );
        GenericProblem gp = ( GenericProblem ) GenericProblem.fromStatusDetail( Response.Status.NOT_FOUND,"No existe el usuario: " + documento );
	gp.getParameters().put( "documento", documento );
 
	throw new NotFoundException( gp );
     }
     ....
     //Ejemplo 3
     } catch ( PersonaException | PersonaNotFoundException e ) {
	log.error( "Se ha producido un error al buscar el usuario [#0].", e, documento );
        GenericProblem gp = ( GenericProblem ) GenericProblem.fromStatusDetail( Response.Status.NOT_FOUND,"No existe el usuario: " + documento );
	gp.getParameters().put( "documento", documento );
	throw gp;
     }
     ....
     ....


Respuesta:

 HTTP/1.1 404 Not Found
 Content-Type: application/problem+json
 
  {
    "type": "about:blank",
    "status": 404,
    "title": "Not Found",
    "detail": "No existe el usuario: persona@um.es",
    "parameters": {
        "documento": "persona@um.es"
    }
  }


4. Excepción y Problem propios

Es posible crear una jerarquía de problemas y excepciones para expresar formal y completamente el conjunto de situaciones de error que pueden suceder en nuestra API REST.

Para crear esta jerarquía es necesario extender las clases GenericProblem y/o WebApplicationException, según sean nuestras necesidades:

  • Si queremos devolver un problem más detallado que proporcione más información sobre el problema ocurrido, hay que extender GenericProblem.
  • Si queremos construir una jerarquía variada de tipos de problemas que dote de mayor semántica a nuestras excepciones y que podamos lanzar fácilmente, hay que extender WebApplicationException o una de sus excepciones hijas definidas en JAX-RS 2.0.


Ejemplo Problem extendido

@XmlRootElement
public class PersonaPruebaProblem extends GenericProblem {
 
	/**
	 * 
	 */
	private static final long serialVersionUID = -990528058685468831L;
 
	public static final URI PERSONA_PRUEBA_TYPE = URI.create( "https://wiki.um.es/persona-prueba-problem" );
 
	private PersonaPrueba persona;
	private boolean activo;
	private boolean existe;
 
	public PersonaPruebaProblem() {
		super();
	}
 
	public PersonaPruebaProblem( Throwable cause ) {
		super( cause );
	}
 
	public PersonaPruebaProblem( URI defaultType, String reasonPhrase, Integer statusCode, String detail ) {
		super( defaultType, reasonPhrase, statusCode, detail );
	}
 
	public PersonaPrueba getPersona() {
		return persona;
	}
 
	public void setPersona( PersonaPrueba persona ) {
		this.persona = persona;
	}
 
	public boolean isActivo() {
		return activo;
	}
 
	public void setActivo( boolean activo ) {
		this.activo = activo;
	}
 
	public boolean isExiste() {
		return existe;
	}
 
	public void setExiste( boolean existe ) {
		this.existe = existe;
	}
 
	public static PersonaPruebaProblem fromStatusDetail( StatusType status, String detail ) {
 
		return new PersonaPruebaProblem( PERSONA_PRUEBA_TYPE, status.getReasonPhrase(), status.getStatusCode(),
				detail );
	}
}
	@GET
	@Path( "/personas/{id}" )
	@Produces( MediaType.APPLICATION_JSON )
	public Response getPersonaPrueba( @Email @PathParam( "id" ) String documento ) throws Throwable {
 
		PersonaPrueba pp = new PersonaPrueba();
		Persona personaBuscada = null;
 
		try {
 
			pp.setCorreo( documento );
			personaBuscada = servicioGenteUmu
					.getPersonaByUsuario( EnvironmentManagerBean.instance().getApplicationName(), documento );
 
			pp.setNombre( personaBuscada.getNombre() );
			pp.setApellido( personaBuscada.getApellidos() );
		} catch ( PersonaException | PersonaNotFoundException e ) {
			log.error( "Se ha producido un error al buscar el usuario [#0].", e, documento );
 
			PersonaPruebaProblem problem = PersonaPruebaProblem
					.fromStatusDetail( Response.Status.NOT_FOUND, "No existe el usuario: " + documento );
			problem.setPersona( pp );
			problem.setActivo( false );
			problem.setExiste( false );
			problem.getParameters().put( "documento", documento );
 
			throw new NotFoundException( problem );
		}
 
		return Response.ok( pp ).build();
	}


Petición:

En este ejemplo, se realiza la petición con un correo inexistente para que el ServicioGenteUmu no lo encuentre y podamos lanzar la excepción JAX-RS NotFoundException.

/personas/persona@um.es


Respuesta:

 HTTP/1.1 404 Not Found
 Content-Type: application/problem+json
 
{
    "type": "https://wiki.um.es/persona-prueba-problem",
    "status": 404,
    "title": "Not Found",
    "detail": "No existe el usuario: persona@um.es",
    "parameters": {
        "documento": "persona@um.es"
    },
    "persona": {
        "nombre": null,
        "apellido": null,
        "movil": null,
        "identificador": null,
        "correo": "persona@um.es"
    },
    "activo": false,
    "existe": false
}


Ejemplo Excepción extendida

public class PersonaPruebaNotFoundException extends NotFoundException {
 
	/**
	 * 
	 */
	private static final long serialVersionUID = 1638710275954917596L;
 
	public static final URI PERSONA_PRUEBA_TYPE = URI.create( "https://wiki.um.es/persona-prueba-problem" );
 
	public PersonaPruebaNotFoundException() {
		super();
	}
 
	public PersonaPruebaNotFoundException( Response response, Throwable cause ) throws IllegalArgumentException {
		super( response, cause );
	}
 
	public PersonaPruebaNotFoundException( Response response ) throws IllegalArgumentException {
		super( response );
	}
 
	public PersonaPruebaNotFoundException( Throwable cause ) {
		super( cause );
 
		if ( GenericProblem.class.isInstance( cause ) ) {
			( ( GenericProblem ) cause ).setType( PERSONA_PRUEBA_TYPE );
		}
	}
}
        @GET
	@Path( "/personas/{id}" )
	@Produces( MediaType.APPLICATION_JSON )
	public Response getPersonaPrueba( @Email @PathParam( "id" ) String documento ) throws Throwable {
 
		PersonaPrueba pp = new PersonaPrueba();
		Persona personaBuscada = null;
 
		try {
 
			pp.setCorreo( documento );
			personaBuscada = servicioGenteUmu
					.getPersonaByUsuario( EnvironmentManagerBean.instance().getApplicationName(), documento );
 
			pp.setNombre( personaBuscada.getNombre() );
			pp.setApellido( personaBuscada.getApellidos() );
		} catch ( PersonaException | PersonaNotFoundException e ) {
			log.error( "Se ha producido un error al buscar el usuario [#0].", e, documento );
 
			throw new PersonaPruebaNotFoundException( ( GenericProblem ) GenericProblem
					.fromStatusDetail( Response.Status.NOT_FOUND, "No existe el usuario: " + documento ) );
		}
 
		return Response.ok( pp ).build();
	}


Petición:

En este ejemplo, se realiza la petición con un correo inexistente para que el ServicioGenteUmu no lo encuentre y podamos lanzar la excepción propia PersonaPruebaNotFoundException.

/personas/persona@um.es


Respuesta:

 HTTP/1.1 404 Not Found
 Content-Type: application/problem+json
 
{
    "type": "https://wiki.um.es/persona-prueba-problem",
    "status": 404,
    "title": "Not Found",
    "detail": "No existe el usuario: persona@um.es",
    "parameters": {}
}

Es posible personalizar la respuesta creando un Mapper que capture las excepciones o problems propios.
Ver la Wiki de Mapeo de excepciones en JAX-RS.

A continuación se describe un Cliente REST JAVA simple para tratar la respuesta según el código de estado HTTP y obtener el GenericProblem en caso de error.

Servicio:

    //....
    } catch ( PersonaException | PersonaNotFoundException e ) {
	log.error( "Se ha producido un error al buscar el usuario [#0].", e, documento );
	GenericProblem gp = ( GenericProblem ) GenericProblem.fromStatusDetail( Response.Status.NOT_FOUND,"No existe el usuario: " + documento );
	gp.getParameters().put( "documento", documento );
 
	throw new NotFoundException( gp );
    }
    //....

Cliente:

        //....
	ClientConfig config = new DefaultClientConfig();
	config.getClasses().add(JacksonJaxbJsonProvider.class);
	config.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
 
	Client cliente = Client.create(config);
	WebResource webResource = cliente.resource( URL_LOCAL_REST ).path( "/personas/persona@um.es" );
 
	ClientResponse respuesta = webResource.accept( MediaType.APPLICATION_JSON ).get( ClientResponse.class );
 
	if (respuesta.getStatus() != 200 ) {
	        GenericProblem p = respuesta.getEntity( GenericProblem.class );
		log.info( "Ha ocurrido un problema consultando persona => #0", p.toString() );
	} else {
		PersonaPrueba pp = respuesta.getEntity( PersonaPrueba.class );
		log.info( "Persona encontrada => #0", pp.toString() );
	}
       //....
INFO (ClienteEjemplo.java:110) - Usuario: mncs@um.es - Ha ocurrido un problema consultando persona => {about:blank,404,Not Found,No existe el usuario: persona@um.es,documento=persona@um.es}


Documentar con Enunciate


Si se usa Enunciate con la configuración por defecto proporcionada en su WiKi, se producirá un error generando la documentación al no encontrar multitud de clases.

Solución
La solución pasa por modificar los path de la tarea ANT que lanza Enunciate para incluir las librerías necesarias.
El código completo puede verse en las FAQs de la WiKi de Enunciate.

RAMON GINEL GEA 03/07/2019 10:14


Pon aquí tus propuestas de FAQs, indicando qué problema tienes, y buscaremos la solución lo antes posible. Si además lo has resuelto, puedes indicar cómo lo has hecho.

Formato de petición de FAQ:

**Título**
//Descripción del problema//
//Tecnología afectada (Fundeweb 1/2)//
//Cómo reproducir el error//
**Solución**
//Descripción de la solución//

Tus datos de contacto  --- //[[correo@umOticarum.es|Tu Nombre]] dd/mm/yyyy //

RAMON GINEL GEA 03/07/2019 10:14

  • fdw2.0/fundeweb2.0/gt/rest/manejo_errores_rest.txt
  • Última modificación: 19/10/2020 09:50
  • por JUAN MIGUEL BERNAL GONZALEZ