JAX-RS – ¿Cómo se devuelven los códigos de estado JSON y HTTP?

Estoy escribiendo una aplicación web REST (NetBeans 6.9, JAX-RS, TopLink Essentials) e bash regresar el código de estado de JSON y HTTP. Tengo el código listo y en funcionamiento que devuelve JSON cuando se llama al método HTTP GET desde el cliente. Esencialmente:

@Path("get/id") @GET @Produces("application/json") public M_機械 getMachineToUpdate(@PathParam("id") String id) { // some code to return JSON ... return myJson; } 

Pero también quiero devolver un código de estado HTTP (500, 200, 204, etc.) junto con los datos JSON.

Traté de usar HttpServletResponse :

 response.sendError("error message", 500); 

Pero esto hizo que el navegador pensara que era un “verdadero” 500 por lo que la página web de salida era una página de error HTTP 500 regular.

Quiero devolver un código de estado HTTP para que mi JavaScript del lado del cliente pueda manejar cierta lógica dependiendo de ello (por ejemplo, mostrar el código de error y el mensaje en una página HTML). ¿Es esto posible o los códigos de estado HTTP no deben usarse para tal cosa?

Aquí hay un ejemplo:

 @GET @Path("retrieve/{uuid}") public Response retrieveSomething(@PathParam("uuid") String uuid) { if(uuid == null || uuid.trim().length() == 0) { return Response.serverError().entity("UUID cannot be blank").build(); } Entity entity = service.getById(uuid); if(entity == null) { return Response.status(Response.Status.NOT_FOUND).entity("Entity not found for UUID: " + uuid).build(); } String json = //convert entity to json return Response.ok(json, MediaType.APPLICATION_JSON).build(); } 

Echa un vistazo a la clase de respuesta .

Tenga en cuenta que siempre debe especificar un tipo de contenido, especialmente si está pasando varios tipos de contenido, pero si cada mensaje se representará como JSON, puede simplemente anotar el método con @Produces("application/json")

Hay varios casos de uso para establecer códigos de estado HTTP en un servicio web REST, y al menos uno no estaba suficientemente documentado en las respuestas existentes (es decir, cuando está utilizando serialización JSON / XML auto-mágica usando JAXB, y desea devolver un objeto a serializar, pero también un código de estado diferente al predeterminado 200).

Así que déjame intentar enumerar los diferentes casos de uso y las soluciones para cada uno:

1. Código de error (500, 404, …)

El caso de uso más común cuando desea devolver un código de estado diferente a 200 OK es cuando ocurre un error.

Por ejemplo:

  • se solicita una entidad pero no existe (404)
  • la solicitud es semánticamente incorrecta (400)
  • el usuario no está autorizado (401)
  • hay un problema con la conexión de la base de datos (500)
  • etc.

a) Lanza una excepción

En ese caso, creo que la forma más limpia de manejar el problema es lanzar una excepción. Esta excepción será manejada por un ExceptionMapper , que traducirá la excepción en una respuesta con el código de error apropiado.

Puede usar el ExceptionMapper predeterminado que viene preconfigurado con Jersey (y supongo que es lo mismo con otras implementaciones) y lanzar cualquiera de las subclases existentes de javax.ws.rs.WebApplicationException . Estos son tipos de excepciones predefinidos que están mapeados previamente para diferentes códigos de error, por ejemplo:

  • BadRequestException (400)
  • InternalServerErrorException (500)
  • NotFoundException (404)

Etc. Puede encontrar la lista aquí: API

Alternativamente, puede definir sus propias excepciones personalizadas y clases de ExceptionMapper , y agregar estos mapeadores a Jersey por medio de la anotación @Provider ( fuente de este ejemplo ):

 public class MyApplicationException extends Exception implements Serializable { private static final long serialVersionUID = 1L; public MyApplicationException() { super(); } public MyApplicationException(String msg) { super(msg); } public MyApplicationException(String msg, Exception e) { super(msg, e); } } 

Proveedor :

  @Provider public class MyApplicationExceptionHandler implements ExceptionMapper { @Override public Response toResponse(MyApplicationException exception) { return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build(); } } 

Nota: también puede escribir ExceptionMappers para los tipos de excepción existentes que utilice.

b) Usa el generador de respuestas

Otra forma de establecer un código de estado es usar un generador de Response para generar una respuesta con el código deseado.

En ese caso, el tipo de devolución de su método debe ser javax.ws.rs.core.Response . Esto se describe en varias otras respuestas como la respuesta aceptada de hisdrewness y se ve así:

 @GET @Path("myresource({id}") public Response retrieveSomething(@PathParam("id") String id) { ... Entity entity = service.getById(uuid); if(entity == null) { return Response.status(Response.Status.NOT_FOUND).entity("Resource not found for ID: " + uuid).build(); } ... } 

2. Éxito, pero no 200

Otro caso en el que desea establecer el estado de devolución es cuando la operación fue exitosa, pero desea devolver un código de éxito diferente a 200, junto con el contenido que devuelve en el cuerpo.

Un caso de uso frecuente es cuando crea una nueva entidad (solicitud POST ) y desea devolver información sobre esta nueva entidad o tal vez la propia entidad, junto con un código de estado 201 Created .

Un enfoque es usar el objeto de respuesta tal como se describe arriba y establecer el cuerpo de la solicitud usted mismo. Sin embargo, al hacerlo, pierde la posibilidad de utilizar la serialización automática en XML o JSON proporcionada por JAXB.

Este es el método original que devuelve un objeto de entidad que JAXB serializará en JSON:

 @Path("/") @POST @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public User addUser(User user){ User newuser = ... do something like DB insert ... return newuser; } 

Esto devolverá una representación JSON del usuario recién creado, pero el estado de devolución será 200, no 201.

Ahora el problema es si quiero usar el generador de Response para establecer el código de retorno, tengo que devolver un objeto de Response en mi método. ¿Cómo devuelvo el objeto User para ser serializado?

a) Establecer el código en la respuesta del servlet

Un enfoque para resolver esto es obtener un objeto de solicitud de servlet y establecer manualmente el código de respuesta, como se demostró en la respuesta de Garett Wilson:

 @Path("/") @POST @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public User addUser(User user, @Context final HttpServletResponse response){ User newUser = ... //set HTTP code to "201 Created" response.setStatus(HttpServletResponse.SC_CREATED); try { response.flushBuffer(); }catch(Exception e){} return newUser; } 

El método aún devuelve un objeto de entidad y el código de estado será 201.

Tenga en cuenta que para que funcione, tuve que descargar la respuesta. Este es un resurgimiento desagradable del código de la API Servlet de bajo nivel en nuestro bonito recurso JAX_RS, y mucho peor, hace que los encabezados sean inmodificables después de esto porque ya fueron enviados por cable.

b) Usa el objeto de respuesta con la entidad

La mejor solución, en ese caso, es usar el objeto Response y establecer la entidad que se serializará en este objeto de respuesta. Sería bueno hacer que el objeto Response sea genérico para indicar el tipo de entidad de carga en ese caso, pero no es el caso actual.

 @Path("/") @POST @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public Response addUser(User user){ User newUser = ... return Response.created(hateoas.buildLinkUri(newUser, "entity")).entity(restResponse).build(); } 

En ese caso, usamos el método creado de la clase de generador de respuestas para establecer el código de estado en 201. Pasamos el objeto de entidad (usuario) a la respuesta mediante el método de entidad ().

El resultado es que el código HTTP es 401 como queríamos, y el cuerpo de la respuesta es el mismo JSON exacto que teníamos antes cuando devolvimos el objeto Usuario. También agrega un encabezado de ubicación.

La clase Response tiene varios métodos de comstackción para diferentes estados (stati?) Como:

Response.accepted () Response.ok () Response.noContent () Response.notAcceptable ()

NB: el objeto hateoas es una clase auxiliar que desarrollé para ayudar a generar URI de recursos. Necesitarás encontrar tu propio mecanismo aquí;)

Eso es todo.

Espero que esta larga respuesta ayude a alguien 🙂

La respuesta por hisdrewness funcionará, pero modifica el enfoque completo para permitir que un proveedor como Jackson + JAXB convierta automáticamente su objeto devuelto a algún formato de salida como JSON. Inspirado por una publicación de Apache CXF (que utiliza una clase específica de CXF), he encontrado una forma de establecer el código de respuesta que debería funcionar en cualquier implementación de JAX-RS: inyectar un contexto HttpServletResponse y configurar manualmente el código de respuesta. Por ejemplo, aquí se explica cómo configurar el código de respuesta a CREATED cuando corresponda.

 @Path("/foos/{fooId}") @PUT @Consumes("application/json") @Produces("application/json") public Foo setFoo(@PathParam("fooID") final String fooID, final Foo foo, @Context final HttpServletResponse response) { //TODO store foo in persistent storage if(itemDidNotExistBefore) //return 201 only if new object; TODO app-specific logic { response.setStatus(Response.Status.CREATED.getStatusCode()); } return foo; //TODO get latest foo from storage if needed } 

Mejora: después de encontrar otra respuesta relacionada, descubrí que se puede inyectar HttpServletResponse como una variable miembro, incluso para la clase de servicio singleton (al menos en RESTEasy). Este es un enfoque mucho mejor que contaminar la API con detalles de implementación. Se vería así:

 @Context //injected response proxy supporting multiple threads private HttpServletResponse response; @Path("/foos/{fooId}") @PUT @Consumes("application/json") @Produces("application/json") public Foo setFoo(@PathParam("fooID") final String fooID, final Foo foo) { //TODO store foo in persistent storage if(itemDidNotExistBefore) //return 201 only if new object; TODO app-specific logic { response.setStatus(Response.Status.CREATED.getStatusCode()); } return foo; //TODO get latest foo from storage if needed } 

Si desea mantener su capa de recursos limpia de objetos de Response , le recomiendo que use @NameBinding y enlace a las implementaciones de ContainerResponseFilter .

Aquí está la carne de la anotación:

 package my.webservice.annotations.status; import javax.ws.rs.NameBinding; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @NameBinding @Retention(RetentionPolicy.RUNTIME) public @interface Status { int CREATED = 201; int value(); } 

Aquí está la carne del filtro:

 package my.webservice.interceptors.status; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; import javax.ws.rs.ext.Provider; import java.io.IOException; @Provider public class StatusFilter implements ContainerResponseFilter { @Override public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException { if (containerResponseContext.getStatus() == 200) { for (Annotation annotation : containerResponseContext.getEntityAnnotations()) { if(annotation instanceof Status){ containerResponseContext.setStatus(((Status) annotation).value()); break; } } } } } 

Y luego la implementación en su recurso simplemente se convierte en:

 package my.webservice.resources; import my.webservice.annotations.status.StatusCreated; import javax.ws.rs.*; @Path("/my-resource-path") public class MyResource{ @POST @Status(Status.CREATED) public boolean create(){ return true; } } 

En caso de que desee cambiar el código de estado debido a una excepción, con JAX-RS 2.0 puede implementar un ExceptionMapper como este. Esto maneja este tipo de excepción para toda la aplicación.

 @Provider public class UnauthorizedExceptionMapper implements ExceptionMapper { @Override public Response toResponse(EJBAccessException exception) { return Response.status(Response.Status.UNAUTHORIZED.getStatusCode()).build(); } } 

JAX-RS tiene soporte para códigos HTTP estándar / personalizados. Ver ResponseBuilder y ResponseStatus, por ejemplo:

http://jackson.codehaus.org/javadoc/jax-rs/1.0/javax/ws/rs/core/Response.ResponseBuilder.html#status%28javax.ws.rs.core.Response.Status%29

Tenga en cuenta que la información JSON es más acerca de los datos asociados con el recurso / aplicación. Los códigos HTTP son más sobre el estado de la operación CRUD solicitada. (al menos así es como se supone que es en sistemas REST-ful)

Si su WS-RS necesita generar un error, ¿por qué no usar la WebApplicationException?

 @GET @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) @Path("{id}") public MyEntity getFoo(@PathParam("id") long id, @QueryParam("lang")long idLanguage) { if (idLanguage== 0){ // No URL parameter idLanguage was sent ResponseBuilder builder = Response.status(Response.Status.BAD_REQUEST); builder.entity("Missing idLanguage parameter on request"); Response response = builder.build(); throw new WebApplicationException(response); } ... //other stuff to return my entity return myEntity; } 

No estoy usando JAX-RS, pero tengo un escenario similar donde uso:

 response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); 

Mire el ejemplo aquí, ilustra mejor el problema y cómo se resuelve en la última versión (2.3.1) de Jersey.

https://jersey.java.net/documentation/latest/representations.html#d0e3586

Básicamente implica definir una excepción personalizada y mantener el tipo de devolución como la entidad. Cuando hay un error, se lanza la excepción; de lo contrario, devuelve el POJO.

Me pareció muy útil construir también un mensaje json con código repetido, como este:

 @POST @Consumes("application/json") @Produces("application/json") public Response authUser(JsonObject authData) { String email = authData.getString("email"); String password = authData.getString("password"); JSONObject json = new JSONObject(); if (email.equalsIgnoreCase(user.getEmail()) && password.equalsIgnoreCase(user.getPassword())) { json.put("status", "success"); json.put("code", Response.Status.OK.getStatusCode()); json.put("message", "User " + authData.getString("email") + " authenticated."); return Response.ok(json.toString()).build(); } else { json.put("status", "error"); json.put("code", Response.Status.NOT_FOUND.getStatusCode()); json.put("message", "User " + authData.getString("email") + " not found."); return Response.status(Response.Status.NOT_FOUND).entity(json.toString()).build(); } } 

Además, tenga en cuenta que, de forma predeterminada, Jersey anulará el cuerpo de la respuesta en caso de un código http de 400 o más.

Para obtener su entidad especificada como cuerpo de respuesta, intente agregar el siguiente init-param a su jersey en su archivo de configuración web.xml:

    jersey.config.server.response.setStatusOverSendError true  

Estoy usando jersey 2.0 con lectores y escritores de cuerpo de mensaje. Tenía mi tipo de devolución de método como una entidad específica que también se utilizó en la implementación del escritor del cuerpo del mensaje y estaba devolviendo el mismo pojo, un SkuListDTO. @GET @Consumes ({“application / xml”, “application / json”}) @Produces ({“application / xml”, “application / json”}) @Path (“/ skuResync”)

 public SkuResultListDTO getSkuData() .... return SkuResultListDTO; 

Todo lo que cambié fue esto, dejé la implementación del escritor solo y todavía funcionaba.

 public Response getSkuData() ... return Response.status(Response.Status.FORBIDDEN).entity(dfCoreResultListDTO).build();