Esta Wiki pretende dar respuesta a las preguntas:
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:
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:
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:
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:
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:
Para el IDE FundeWeb 2.0 está disponible a partir de la siguiente versión de las librerias:
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.
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.
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.
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.
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": {} }
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.
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" } }
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:
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}
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