====== Manejo de errores en servicios REST ====== ===== Introducción ===== 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: * Especificación [[https://tools.ietf.org/html/rfc7807|RFC 7807 Problem Details for HTTP APIs]]. * [[fdw2.0:fundeweb2.0:gt:rest:mapeo_excepciones_rest|Mapeo de excepciones en JAX-RS]]. * [[fdw2.0:fundeweb2.0:gt:rest:guia_validacion_servicio_rest|Validación de métodos (Bean Validation)]]. \\ ===== Buenas prácticas en respuestas de error ===== 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 [[https://es.wikipedia.org/wiki/Anexo:C%C3%B3digos_de_estado_HTTP|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**. \\ ===== Especificación Problem (Problem Details for HTTP APIs) ===== En los últimos años se ha popularizado el uso de la especificación __[[https://tools.ietf.org/html/rfc7807|RFC 7807]]__ para definir un **formato común en las respuestas de error** para APIs HTTP. \\ Podemos describir un objeto [[https://tools.ietf.org/html/rfc7807#section-3.1|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**. \\ ===== Manejo de Errores en FundeWeb ===== 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: Para poder utilizarlos, tenemos que añadirlos a un elemento //// o ////. 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 [[fdw2.0:fundeweb2.0:gt:rest:guia_validacion_servicio_rest|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 [[fdw2.0:fundeweb2.0:gt:rest:mapeo_excepciones_rest|Wiki de Mapeo de excepciones en JAX-RS]]. ===== Cliente de Ejemplo ===== 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} ===== Referencias y Bibliografía ===== * https://tools.ietf.org/html/rfc7807 * https://dennis-xlc.gitbooks.io/restful-java-with-jax-rs-2-0-en/cn/part1/chapter7/exception_handling.html * https://jersey.github.io/documentation/latest/bean-validation.html * https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/bean-validation.html#d0e13664 ===== FAQs ===== \\ === Documentar con Enunciate === \\ Si se usa Enunciate con la configuración por defecto proporcionada en su [[fdw2.0:fundeweb2.0:gt:documentar-rest-enunciate|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 [[fdw2.0:fundeweb2.0:gt:documentar-rest-enunciate#faqs_resueltas|FAQs de la WiKi de Enunciate]]. --- //[[ramon.ginel@ticarum.es|RAMON GINEL GEA]] 03/07/2019 10:14// \\ ===== Solicitud/Registro de FAQ ===== 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@ticarum.es|RAMON GINEL GEA]] 03/07/2019 10:14//