ExceptionMapper y tratamiento de errores

ExceptionMapper es una alternativa para el tratamiento de errores con el que podemos hacer que en caso de lanzarse una excepción por parte de alguno de los endpoints que hemos metido en nuestra API, se convierta en la respuesta correspondiente. Así podemos fabricar un conjunto de respuestas reusables asociadas a excepciones concretas que se comparten por toda la aplicación.

Si la lógica de negocio de nuestra aplicación generase excepciones que no son tratados, normalmente vemos un mensaje de error de color rojo en el navegador web, u obtenemos una traza terrorífica si consumimos la versión JSON o XML de la API:

Traza de error de Quarkus

Si bien podemos poner un tratamiento de errores dentro de cada uno de los métodos de un controlador, y podemos aprovecharnos de la posibilidad de crear respuestas personalizadas en función de una condición, como vimos en la lección donde se habló de Response y ResponseBuilder, otra cosa que podemos hacer en su lugar es crear conversores globales de excepciones, o ExceptionMappers.

Un ExceptionMapper es una clase que una vez registrada en nuestra aplicación va a permitir transformar una excepción en un objeto de tipo Response. De este modo, podemos registrar en nuestra aplicación un ExceptionMapper para tratar excepciones de tipo NullPointerException. A partir de ahí, para cualquier endpoint de la aplicación que tire un NullPointerException, siempre se llamará al mapper para generar un objeto Response más limpio que usar en nuestra API.

Es relativamente simple de hacer. Creamos una clase que tiene que implementar la interfaz ExceptionMapper<E extends Throwable>. Esta interfaz la puedes importar con un import jakarta.ws.rs.ext.ExceptionMapper, y está parametrizada por un genérico de tipo Throwable, así que tendremos que indicar qué excepción es la que queremos tratar. Por ejemplo:

public class NoSuchElementExceptionMapper
  implements ExceptionMapper<NoSuchElementException>
{
  // Este es mi mapper para tratar un NoSuchElementException
}

public class NullPointerExceptionMapper
  implements ExceptionMapper<NullPointerException>
{
  // Este es mi mapper para tratar un NullPointerException
}

public class TemperaturaDuplicadaExceptionMapper
  implements ExceptionMapper<TemperaturaDuplicadaException>
{
  // Este es mi mapper para tratar un TemperaturaDuplicadaException
}

La interfaz exigirá implementar un método denominado toResponse(), que si miramos la definición está especificado como Response toResponse(E exception);, es decir, al implementar ese método recibiremos como parámetro la excepción que estamos tratando y nosotros tenemos que fabricar un Response para generar una versión limpia de ese mensaje de error.

Tan simple como hacer algo como esto:

@Provider
public class NoSuchElementExceptionMapper
  implements ExceptionMapper<NoSuchElementException>
{
  @Override
  public Response toResponse(NoSuchElementException e) {
    String error = "No se encontró: " + e.getMessage();
    return Response.status(404).entity(error).build();
  }
}

Por último, observa que a la clase le he puesto la anotación @Provider. Esta anotación la encuentras con un import jakarta.ws.rs.ext.Provider; y hace falta para señalizarle a Quarkus que es importante a la hora de arrancar la aplicación.

Con todo, a partir de ahora si un endpoint genera una excepción de tipo NoSuchElementException, la respuesta que se devolverá de la API no será el temido pantallazo rojo ni tampoco una traza de excepción gigante, sino que se llamará al método toResponse() de nuestra clase transformadora y se devolverá esa respuesta. En este caso, bien podría ser el mensaje "No se encontró: La ciudad Valencia no está dada de alta".

Generalmente, para darle más utilidad se suele fabricar una pequeña clase de datos usada para poder serializar a JSON cómodamente. Con registros esto es muy fácil de conseguir:

@Provider
public class IllegalArgumentExceptionMapper
  implements ExceptionMapper<IllegalArgumentException>
{
  private record ExceptionDto(int statusCode, String detail) {
    public static ExceptionDto codificar(int statusCode, Throwable t) {
      return new ExceptionDto(statusCode, t.getMessage());
    }
  }

  @Override
  public Response toResponse(IllegalArgumentException exception) {
    var respuesta = ExceptionDto.codificar(422, exception);
    return Response.status(respuesta.statusCode).entity(respuesta).build();
  }
}

A partir de ahora, en caso de error la API devolverá la entidad codificada en el toResponse.

Lista de reproducción
  1. 1
    Qué es Quarkus y cómo crear un proyecto
    8 minutos
  2. 2
    ¿Cómo crear endpoints en Quarkus?
    7 minutos
  3. 3
    Paso de parámetros con PathParam y QueryParam
    8 minutos
  4. 4
    Retorno de objetos JSON
    7 minutos
  5. 5
    Definir un endpoint POST
    7 minutos
  6. 6
    Servicios e inyección de dependencia
    8 minutos
  7. 7
    Response y ResponseBuilder
    8 minutos
  8. 8
    ExceptionMapper y tratamiento de errores
    9 minutos