JAX-RS / Jersey ¿cómo personalizar el manejo de errores?

216

Estoy aprendiendo JAX-RS (también conocido como JSR-311) usando Jersey. He creado con éxito un recurso raíz y estoy jugando con los parámetros:

@Path("/hello")
public class HelloWorldResource {

    @GET
    @Produces("text/html")
    public String get(
        @QueryParam("name") String name,
        @QueryParam("birthDate") Date birthDate) {

         // Return a greeting with the name and age
    }
}

Esto funciona muy bien y maneja cualquier formato en el entorno local actual que entiende el constructor Fecha (Cadena) (como AAAA / mm / dd y mm / dd / AAAA). Pero si proporciono un valor que no es válido o no se entiende, obtengo una respuesta 404.

Por ejemplo:

GET /hello?name=Mark&birthDate=X

404 Not Found

¿Cómo puedo personalizar este comportamiento? ¿Quizás un código de respuesta diferente (probablemente "400 Solicitud incorrecta")? ¿Qué pasa con el registro de un error? ¿Quizás agregar una descripción del problema ("formato de fecha incorrecta") en un encabezado personalizado para ayudar a solucionar problemas? ¿O devolver una respuesta de error completa con detalles, junto con un código de estado 5xx?

Mark Renouf
fuente

Respuestas:

271

Existen varios enfoques para personalizar el comportamiento de manejo de errores con JAX-RS. Aquí hay tres de las formas más fáciles.

El primer enfoque es crear una clase de excepción que extienda WebApplicationException.

Ejemplo:

public class NotAuthorizedException extends WebApplicationException {
     public NotAuthorizedException(String message) {
         super(Response.status(Response.Status.UNAUTHORIZED)
             .entity(message).type(MediaType.TEXT_PLAIN).build());
     }
}

Y para lanzar esta Excepción recién creada, simplemente:

@Path("accounts/{accountId}/")
    public Item getItem(@PathParam("accountId") String accountId) {
       // An unauthorized user tries to enter
       throw new NotAuthorizedException("You Don't Have Permission");
}

Tenga en cuenta que no necesita declarar la excepción en una cláusula throws porque WebApplicationException es una excepción de tiempo de ejecución. Esto devolverá una respuesta 401 al cliente.

El segundo y más fácil enfoque es simplemente construir una instancia del WebApplicationExceptiondirectamente en su código. Este enfoque funciona siempre que no tenga que implementar sus propias excepciones de aplicación.

Ejemplo:

@Path("accounts/{accountId}/")
public Item getItem(@PathParam("accountId") String accountId) {
   // An unauthorized user tries to enter
   throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}

Este código también devuelve un 401 al cliente.

Por supuesto, este es solo un ejemplo simple. Puede hacer que la Excepción sea mucho más compleja si es necesario, y puede generar cualquier código de respuesta http que necesite.

Otro enfoque es envolver una Excepción existente, tal vez una ObjectNotFoundExceptioncon una clase de contenedor pequeña que implemente la ExceptionMapperinterfaz anotada con una @Provideranotación. Esto le dice al tiempo de ejecución JAX-RS, que si se genera la excepción envuelta, devuelve el código de respuesta definido en ExceptionMapper.

Steven Levine
fuente
3
En su ejemplo, la llamada a super () debería ser ligeramente diferente: super (Response.status (Status.UNAUTHORIZED). Entity (message) .type ("text / plain"). Build ()); Gracias por la idea sin embargo.
Jon Onstott
65
En el escenario mencionado en la pregunta, no tendrá la oportunidad de lanzar una excepción, ya que Jersey generará una excepción ya que no podrá crear una instancia del objeto Date a partir del valor de entrada. ¿Hay alguna manera de interceptar la excepción de Jersey? Hay una interfaz ExceptionMapper, sin embargo, que también intercepta las excepciones lanzadas por el método (get en este caso).
Rejeev Divakaran
77
¿Cómo evita que aparezca la excepción en los registros de su servidor si el 404 es un caso válido y no un error (es decir, cada vez que consulta un recurso, solo para ver si ya existe, con su enfoque aparece un seguimiento de pila en el servidor registros).
Guido
3
Vale la pena mencionar que Jersey 2.x define excepciones para algunos de los códigos de error HTTP más comunes. Entonces, en lugar de definir sus propias subclases de WebApplication, puede usar las integradas como BadRequestException y NotAuthorizedException. Mire las subclases de javax.ws.rs.ClientErrorException por ejemplo. También tenga en cuenta que puede proporcionar una cadena de detalles a los constructores. Por ejemplo: lanzar una nueva BadRequestException ("La fecha de inicio debe preceder a la fecha de finalización");
Bampfer
1
olvidó mencionar otro enfoque más: implementar la ExceptionMapperinterfaz (que es un enfoque mejor que extenderlo). Ver más aquí vvirlan.wordpress.com/2015/10/19/…
ACV
70
@Provider
public class BadURIExceptionMapper implements ExceptionMapper<NotFoundException> {

public Response toResponse(NotFoundException exception){

    return Response.status(Response.Status.NOT_FOUND).
    entity(new ErrorResponse(exception.getClass().toString(),
                exception.getMessage()) ).
    build();
}
}

Crear por encima de la clase. Esto manejará 404 (NotFoundException) y aquí en el método toResponse puede dar su respuesta personalizada. Del mismo modo, hay ParamException, etc., que necesitaría asignar para proporcionar respuestas personalizadas.

Arnav
fuente
Puede usar implementos ExceptionMapper <Exception> también para excepciones genéricas
Saurabh
1
Esto manejaría WebApplicationExceptions lanzadas por JAX-RS Client también, ocultando el origen del error. Es mejor tener una excepción personalizada (no derivada de WebApplicationException) o lanzar aplicaciones web con respuesta completa. Las excepciones de aplicación web lanzadas por el cliente JAX-RS deben manejarse directamente en la llamada, de lo contrario, la respuesta de otro servicio se transmite como respuesta de su servicio, aunque es un error interno no controlado del servidor.
Markus Kull
38

Jersey arroja una com.sun.jersey.api.ParamException cuando no logra desarmar los parámetros, por lo que una solución es crear un ExceptionMapper que maneje estos tipos de excepciones:

@Provider
public class ParamExceptionMapper implements ExceptionMapper<ParamException> {
    @Override
    public Response toResponse(ParamException exception) {
        return Response.status(Status.BAD_REQUEST).entity(exception.getParameterName() + " incorrect type").build();
    }
}
Jan Kronquist
fuente
¿Dónde debo crear este mapeador específicamente para que Jersey lo registre?
Patricio
1
Todo lo que tiene que hacer es agregar la anotación @Provider, consulte aquí para obtener más detalles: stackoverflow.com/questions/15185299/…
Jan Kronquist
27

También podría escribir una clase reutilizable para variables anotadas QueryParam

public class DateParam {
  private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

  private Calendar date;

  public DateParam(String in) throws WebApplicationException {
    try {
      date = Calendar.getInstance();
      date.setTime(format.parse(in));
    }
    catch (ParseException exception) {
      throw new WebApplicationException(400);
    }
  }
  public Calendar getDate() {
    return date;
  }
  public String format() {
    return format.format(value.getTime());
  }
}

luego úsalo así:

private @QueryParam("from") DateParam startDateParam;
private @QueryParam("to") DateParam endDateParam;
// ...
startDateParam.getDate();

Aunque el manejo de errores es trivial en este caso (arrojando una respuesta 400), el uso de esta clase le permite factorizar el manejo de parámetros en general, lo que podría incluir el registro, etc.

Charlie Brooking
fuente
Estoy tratando de agregar un controlador de parámetros de consulta personalizado en Jersey (migrando desde CXF), esto se ve notablemente similar a lo que estoy haciendo, pero no sé cómo instalar / crear un nuevo proveedor. Tu clase de arriba no me muestra esto. Estoy usando objetos JodaTime DateTime para QueryParam y no tengo un proveedor para decodificarlos. ¿Es tan fácil como subclasificarlo, darle un constructor de cadena y manejar eso?
Christian Bongiorno
1
Simplemente cree una clase como la DateParamanterior que envuelva un en org.joda.time.DateTimelugar de java.util.Calendar. Usas eso con en @QueryParamlugar de DateTimesí mismo.
Charlie Brooking
1
Si está utilizando Joda DateTime, jersey viene con DateTimeParam para que lo use directamente. No es necesario que escribas el tuyo. Ver github.com/dropwizard/dropwizard/blob/master/dropwizard-jersey/…
Srikanth
Voy a agregar esto porque es muy útil, pero solo si estás usando Jackson con Jersey. Jackson 2.x tiene un JodaModuleque se puede registrar con el ObjectMapper registerModulesmétodo. Puede manejar todas las conversiones de tipo joda. com.fasterxml.jackson.datatype.joda.JodaModule
j_walker_dev
11

Una solución obvia: tome una Cadena, conviértala en Fecha usted mismo. De esa manera puede definir el formato que desee, capturar excepciones y volver a lanzar o personalizar el error que se envía. Para el análisis, SimpleDateFormat debería funcionar bien.

Estoy seguro de que también hay formas de conectar controladores para tipos de datos, pero tal vez un poco de código simple es todo lo que necesita en este caso.

StaxMan
fuente
7

También me gusta que StaxMan probablemente implemente ese QueryParam como una Cadena, luego maneje la conversión, volviendo a lanzarla según sea necesario.

Si el comportamiento específico de la localidad es el comportamiento deseado y esperado, usaría lo siguiente para devolver el error 400 MALA SOLICITUD:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Consulte JavaDoc para javax.ws.rs.core.Response.Status para obtener más opciones.

dshaw
fuente
4

La documentación de @QueryParam dice

"El tipo T del parámetro, campo o propiedad anotado debe:

1) Sea un tipo primitivo
2) Tenga un constructor que acepte un solo argumento de String
3) Tenga un método estático llamado valueOf o fromString que acepte un solo argumento de String (vea, por ejemplo, Integer.valueOf (String))
4) Tenga un implementación registrada de la extensión SPI javax.ws.rs.ext.ParamConverterProvider JAX-RS que devuelve una instancia de javax.ws.rs.ext.ParamConverter capaz de realizar una conversión "de cadena" para el tipo.
5) Sea List, Set u SortedSet, donde T satisface 2, 3 o 4 arriba. La colección resultante es de solo lectura. "

Si desea controlar qué respuesta va al usuario cuando el parámetro de consulta en forma de cadena no se puede convertir a su tipo T, puede lanzar WebApplicationException. Dropwizard viene con las siguientes clases de * Param que puedes usar para tus necesidades.

BooleanParam, DateTimeParam, IntParam, LongParam, LocalDateParam, NonEmptyStringParam, UUIDParam. Ver https://github.com/dropwizard/dropwizard/tree/master/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params

Si necesita Joda DateTime, simplemente use Dropwizard DateTimeParam .

Si la lista anterior no satisface sus necesidades, defina la suya ampliando AbstractParam. Anular el método de análisis. Si necesita control sobre el cuerpo de respuesta de error, anule el método de error.

Un buen artículo de Coda Hale sobre esto está en http://codahale.com/what-makes-jersey-interesting-parameter-classes/

import io.dropwizard.jersey.params.AbstractParam;

import java.util.Date;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

public class DateParam extends AbstractParam<Date> {

    public DateParam(String input) {
        super(input);
    }

    @Override
    protected Date parse(String input) throws Exception {
        return new Date(input);
    }

    @Override
    protected Response error(String input, Exception e) {
        // customize response body if you like here by specifying entity
        return Response.status(Status.BAD_REQUEST).build();
    }
}

El constructor de fecha (String arg) está en desuso. Usaría clases de fecha Java 8 si está en Java 8. De lo contrario, se recomienda la fecha de joda.

Srikanth
fuente
1

Este es el comportamiento correcto en realidad. Jersey intentará encontrar un controlador para su entrada e intentará construir un objeto a partir de la entrada proporcionada. En este caso, intentará crear un nuevo objeto Date con el valor X proporcionado al constructor. Como esta es una fecha no válida, por convención Jersey devolverá 404.

Lo que puede hacer es reescribir y poner la fecha de nacimiento como una Cadena, luego intentar analizar y, si no obtiene lo que desea, puede lanzar cualquier excepción que desee mediante cualquiera de los mecanismos de mapeo de excepciones (hay varios )

ACV
fuente
1

Estaba enfrentando el mismo problema.

Quería detectar todos los errores en un lugar central y transformarlos.

El siguiente es el código de cómo lo manejé.

Cree la siguiente clase que implementa ExceptionMappery agregue @Provideranotaciones en esta clase. Esto manejará todas las excepciones.

Anule el toResponsemétodo y devuelva el objeto de Respuesta poblado con datos personalizados.

//ExceptionMapperProvider.java
/**
 * exception thrown by restful endpoints will be caught and transformed here
 * so that client gets a proper error message
 */
@Provider
public class ExceptionMapperProvider implements ExceptionMapper<Throwable> {
    private final ErrorTransformer errorTransformer = new ErrorTransformer();

    public ExceptionMapperProvider() {

    }

    @Override
    public Response toResponse(Throwable throwable) {
        //transforming the error using the custom logic of ErrorTransformer 
        final ServiceError errorResponse = errorTransformer.getErrorResponse(throwable);
        final ResponseBuilder responseBuilder = Response.status(errorResponse.getStatus());

        if (errorResponse.getBody().isPresent()) {
            responseBuilder.type(MediaType.APPLICATION_JSON_TYPE);
            responseBuilder.entity(errorResponse.getBody().get());
        }

        for (Map.Entry<String, String> header : errorResponse.getHeaders().entrySet()) {
            responseBuilder.header(header.getKey(), header.getValue());
        }

        return responseBuilder.build();
    }
}

// ErrorTransformer.java
/**
 * Error transformation logic
 */
public class ErrorTransformer {
    public ServiceError getErrorResponse(Throwable throwable) {
        ServiceError serviceError = new ServiceError();
        //add you logic here
        serviceError.setStatus(getStatus(throwable));
        serviceError.setBody(getBody(throwable));
        serviceError.setHeaders(getHeaders(throwable));

    }
    private String getStatus(Throwable throwable) {
        //your logic
    }
    private Optional<String> getBody(Throwable throwable) {
        //your logic
    }
    private Map<String, String> getHeaders(Throwable throwable) {
        //your logic
    }
}

//ServiceError.java
/**
 * error data holder
 */
public class ServiceError {
    private int status;
    private Map<String, String> headers;
    private Optional<String> body;
    //setters and getters
}
suraj.tripathi
fuente
1

Enfoque 1: ampliando la clase WebApplicationException

Cree una nueva excepción extendiendo WebApplicationException

public class RestException extends WebApplicationException {

         private static final long serialVersionUID = 1L;

         public RestException(String message, Status status) {
         super(Response.status(status).entity(message).type(MediaType.TEXT_PLAIN).build());
         }
}

Ahora lanza 'RestException' cuando sea necesario.

public static Employee getEmployee(int id) {

         Employee emp = employees.get(id);

         if (emp == null) {
                 throw new RestException("Employee with id " + id + " not exist", Status.NOT_FOUND);
         }
         return emp;
}

Puede ver la solicitud completa en este enlace .

Enfoque 2: Implementar ExceptionMapper

El siguiente mapeador maneja la excepción del tipo 'DataNotFoundException'

@Provider
public class DataNotFoundExceptionMapper implements
        ExceptionMapper<DataNotFoundException> {

    @Override
    public Response toResponse(DataNotFoundException ex) {
        ErrorMessage model = new ErrorMessage(ex.getErrorCode(),
                ex.getMessage());
        return Response.status(Status.NOT_FOUND).entity(model).build();
    }

}

Puede ver la solicitud completa en este enlace .

Hari Krishna
fuente
0

Solo como una extensión de la respuesta de @Steven Lavine en caso de que desee abrir la ventana de inicio de sesión del navegador. Me resultó difícil devolver correctamente la Respuesta ( Autenticación HTTP MDN ) del Filtro en caso de que el usuario aún no se haya autenticado

Esto me ayudó a construir la Respuesta para forzar el inicio de sesión del navegador, tenga en cuenta la modificación adicional de los encabezados. Esto establecerá el código de estado en 401 y establecerá el encabezado que hace que el navegador abra el diálogo de nombre de usuario / contraseña.

// The extended Exception class
public class NotLoggedInException extends WebApplicationException {
  public NotLoggedInException(String message) {
    super(Response.status(Response.Status.UNAUTHORIZED)
      .entity(message)
      .type(MediaType.TEXT_PLAIN)
      .header("WWW-Authenticate", "Basic realm=SecuredApp").build()); 
  }
}

// Usage in the Filter
if(headers.get("Authorization") == null) { throw new NotLoggedInException("Not logged in"); }
Omnibyte
fuente