JAX-RS - ¿Cómo devolver el código de estado JSON y HTTP juntos?


248

Estoy escribiendo una aplicación web REST (NetBeans 6.9, JAX-RS, TopLink Essentials) e intento devolver el código de estado JSON y HTTP. Tengo un código listo y funcionando 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 es un 500 "real", por lo que la página web de salida era una página de error HTTP 500 normal.

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 no deberían utilizarse los códigos de estado HTTP para tal cosa?


2
¿Cuál es la diferencia entre 500 que quieres (irreal? :)) y 500 reales?
afeitadora

@razor Aquí real 500 significa una página de error HTML en lugar de respuesta JSON
Nupur

el navegador web no fue diseñado para funcionar con JSON, sino con páginas HTML, por lo que si responde con 500 (e incluso algún cuerpo de mensaje), el navegador puede mostrarle solo un mensaje de error (realmente depende de la implementación del navegador), solo porque eso es útil para Un usuario normal.
afeitadora

Respuestas:


347

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")


12
Funciona, pero lo que no me gusta del valor de respuesta de Response es que, en mi opinión, contamina su código, especialmente con respecto a cualquier cliente que intente usarlo. Si proporciona una interfaz que devuelve una Respuesta a un tercero, él no sabe qué tipo realmente está devolviendo. Spring lo deja más claro con una anotación, muy útil si siempre devuelve un código de estado (es decir, HTTP 204)
Guido

19
Hacer que esa clase sea genérica (Respuesta <T>) sería una mejora interesante para jax-rs, para tener las ventajas de ambas alternativas.
Guido

41
No hay necesidad de convertir la entidad a json de alguna manera. Puede hacer return Response.status(Response.Status.Forbidden).entity(myPOJO).build();Works como si lo hiciera return myPOJO;, pero con una configuración adicional del código de estado HTTP.
kratenko

1
Creo que separar la lógica de negocios en una clase de servicio separada funciona bien. El punto final utiliza Response como tipo de retorno y sus métodos son principalmente llamadas a los métodos de servicio más la ruta y las anotaciones de parámetros. Separa limpiamente la lógica del mapeo de tipo url / contenido (donde el caucho golpea la carretera, por así decirlo).
Stijn de Witt

en realidad, uno puede devolver el objeto que no se ajusta a la Respuesta.
ses

191

Hay varios casos de uso para configurar 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 la serialización JSON / XML auto-mágica utilizando JAXB, y desea devolver un objeto para ser serializado, pero también un código de estado diferente al predeterminado 200).

Permítanme 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 de 200 OKcuando 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) Lanzar 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 valor predeterminado ExceptionMapperque viene preconfigurado con Jersey (y supongo que es lo mismo con otras implementaciones) y arrojar cualquiera de las subclases existentes de javax.ws.rs.WebApplicationException. Estos son tipos de excepción predefinidos que se asignan previamente a 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 y ExceptionMapperclases personalizadas, y agregar estos mapeadores a Jersey por medio de la @Provideranotación ( 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<MyApplicationException> 
    {
        @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 usa.

b) Use el generador de respuestas

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

En ese caso, el tipo de retorno 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 retorno es cuando la operación fue exitosa, pero desea devolver un código de éxito diferente de 200, junto con el contenido que devuelve en el cuerpo.

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

Un enfoque es utilizar el objeto de respuesta tal como se describió anteriormente y establecer el cuerpo de la solicitud usted mismo. Sin embargo, al hacer esto, pierde la capacidad de utilizar la serialización automática a XML o JSON proporcionada por JAXB.

Este es el método original que devuelve un objeto de entidad que JAXB serializará a 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 que si quiero usar el Responsegenerador para configurar el código de retorno, tengo que devolver un Responseobjeto en mi método. ¿Cómo devuelvo el Userobjeto para que se serialice?

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

Un enfoque para resolver esto es obtener un objeto de solicitud de servlet y establecer el código de respuesta manualmente nosotros mismos, como se demuestra 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 vaciar la respuesta. Este es un resurgimiento desagradable del código API de Servlet de bajo nivel en nuestro agradable recurso JAX_RS, y mucho peor, hace que los encabezados no se puedan modificar después de esto porque ya se enviaron por cable.

b) Use el objeto de respuesta con la entidad

La mejor solución, en ese caso, es usar el objeto de Respuesta y configurar la entidad para que se serialice en este objeto de respuesta. Sería bueno hacer que el objeto Respuesta sea genérico para indicar el tipo de entidad de carga útil 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, utilizamos el método creado de la clase de generador de Respuesta para establecer el código de estado en 201. Pasamos el objeto de entidad (usuario) a la respuesta a través del método de entidad ().

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

La clase Response tiene varios métodos de creació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 recursos URI. Tendrá que crear su propio mecanismo aquí;)

Eso es todo.

Espero que esta larga respuesta ayude a alguien :)


Me pregunto si hay una manera limpia de devolver el objeto de datos en lugar de la respuesta. El flushes de hecho sucio.
AlikElzin-kilaka 01 de

1
Solo un motivo favorito mío: 401 no significa que el usuario no esté autorizado. Significa que el cliente no está autorizado, porque el servidor no sabe quién es usted. Si un usuario registrado / reconocido de otra manera no puede realizar una determinada acción, el código de respuesta correcto es 403 Prohibido.
Demonblack

69

La respuesta de hisdrewness funcionará, pero modifica todo el enfoque 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 Apache CXF (que usa una clase específica de CXF), he encontrado una forma de configurar el código de respuesta que debería funcionar en cualquier implementación 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 CREATEDcuando sea apropiado.

@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 , aprendí que se puede inyectar la HttpServletResponsevariable como 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
}

En realidad, puede combinar los enfoques: anote el método con @Produces, y no especifique el tipo de medio en la final Response.ok, y obtendrá su objeto de retorno correctamente serializado en JAXB en el tipo de medio apropiado para que coincida con la solicitud. (Acabo de probar esto con un único método que podría devolver XML o JSON: el método en sí no necesita mencionar ninguno, excepto en la @Producesanotación.)
Royston Shufflebotham

Tienes razón Garret. Mi ejemplo fue más una ilustración del énfasis de proporcionar un tipo de contenido. Nuestros enfoques son similares, pero la idea de usar una MessageBodyWritery Providerpermite la negociación de contenido implícito, aunque parece que su ejemplo le falta algún código. Aquí hay otra respuesta que proporcioné que ilustra esto: stackoverflow.com/questions/5161466/…
hisdrewness

8
No puedo anular el código de estado en response.setStatus(). La única forma de enviar, por ejemplo, una respuesta 404 No encontrado es establecer el código de estado de respuesta response.setStatus(404)y luego cerrar la secuencia de salida response.getOutputStream().close()para que JAX-RS no pueda restablecer mi estado.
Rob Juurlink

2
Pude usar este enfoque para establecer un código 201, pero tuve que agregar un bloque try-catch response.flushBuffer()para evitar que el marco anulara mi código de respuesta. No muy limpio
Pierre Henry

1
@RobJuurlink, si desea devolver específicamente un 404 Not Found, puede ser más fácil de usar throw new NotFoundException().
Garret Wilson

34

Si desea mantener su capa de recursos limpia de Responseobjetos, le recomiendo que use @NameBindingy enlace a 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;
  }
}

Mantiene la API limpia, buena respuesta. ¿Sería posible parametrizar su anotación como @Status (código = 205) y hacer que el interceptor reemplace el código con lo que especifique? Creo que básicamente le daría una anotación para anular los códigos siempre que lo necesite.
user2800708

@ user2800708, ya hice esto para mi código local, actualicé la respuesta como sugeriste.
Nthalk 01 de

Genial gracias. Con esto y algunos otros trucos, ahora básicamente puedo limpiar las API REST en mi código, de modo que se ajuste a una interfaz Java simple sin mencionar REST; es solo otro mecanismo de RMI.
user2800708

66
En lugar de repetir anotaciones en StatusFilter, puede anotar el filtro con @ Status además de @ Provider. Entonces el filtro solo se invocará en los recursos que están anotados con @ Status. Este es el propósito de @ NameBinding
trevorism

1
Buen llamado @trevorism. Hay un efecto secundario no tan agradable de anotar StatusFiltercon @Status: debe proporcionar un valor predeterminado para el valuecampo de la anotación , o declarar uno al anotar la clase (por ejemplo:) @Status(200). Esto obviamente no es ideal.
Phil

6

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<EJBAccessException> {

    @Override
    public Response toResponse(EJBAccessException exception) {
        return Response.status(Response.Status.UNAUTHORIZED.getStatusCode()).build();
    }

}

6

Si su WS-RS necesita generar un error, ¿por qué no usar la excepción 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;
}

44
En mi opinión, WebApplicationExceptions no es adecuado para errores del lado del cliente porque arrojan grandes rastros de pila. Los errores del cliente no deben arrojar rastros de la pila del lado del servidor y contaminar el registro con él.
Rob Juurlink

5

JAX-RS tiene soporte para códigos HTTP estándar / personalizados. Consulte 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 sobre los datos asociados con el recurso / aplicación. Los códigos HTTP tratan más sobre el estado de la operación CRUD que se solicita. (al menos así es como se supone que debe ser en los sistemas REST-ful)


el enlace está roto
Umpa

5

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();
    }
}

4

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 retorno como la entidad. Cuando hay un error, se lanza la excepción, de lo contrario, devuelve el POJO.


Me gustaría agregar que el ejemplo de interés es aquel en el que definen su propia clase de excepción y crean una Responseen ella. Simplemente busque la CustomNotFoundExceptionclase y tal vez cópiela en su publicación.
JBert

Utilizo este enfoque para errores y me gusta. Pero no es aplicable a los códigos de éxito (diferentes de 200) como '201 creado'.
Pierre Henry

3

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

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

Lo hace para mí usando Spring MVC, ¡pero hay una manera fácil de descubrirlo!
Gorras

1

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

Para obtener su entidad especificada como el cuerpo de respuesta, intente agregar el siguiente parámetro init a su Jersey en su archivo de configuración web.xml:

    <init-param>
        <!-- used to overwrite default 4xx state pages -->
        <param-name>jersey.config.server.response.setStatusOverSendError</param-name>
        <param-value>true</param-value>
    </init-param>

0

El siguiente código funcionó para mí. Inyectando el mensajeContexto a través de un anotador establecido y configurando el código de estado en mi método "agregar".

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;

import org.apache.cxf.jaxrs.ext.MessageContext;

public class FlightReservationService {

    MessageContext messageContext;

    private final Map<Long, FlightReservation> flightReservations = new HashMap<>();

    @Context
    public void setMessageContext(MessageContext messageContext) {
        this.messageContext = messageContext;
    }

    @Override
    public Collection<FlightReservation> list() {
        return flightReservations.values();
    }

    @Path("/{id}")
    @Produces("application/json")
    @GET
    public FlightReservation get(Long id) {
        return flightReservations.get(id);
    }

    @Path("/")
    @Consumes("application/json")
    @Produces("application/json")
    @POST
    public void add(FlightReservation booking) {
        messageContext.getHttpServletResponse().setStatus(Response.Status.CREATED.getStatusCode());
        flightReservations.put(booking.getId(), booking);
    }

    @Path("/")
    @Consumes("application/json")
    @PUT
    public void update(FlightReservation booking) {
        flightReservations.remove(booking.getId());
        flightReservations.put(booking.getId(), booking);
    }

    @Path("/{id}")
    @DELETE
    public void remove(Long id) {
        flightReservations.remove(id);
    }
}

0

Ampliando la respuesta de Nthalk con Microprofile OpenAPI , puede alinear el código de retorno con su documentación utilizando la anotación @APIResponse .

Esto permite etiquetar un método JAX-RS como

@GET
@APIResponse(responseCode = "204")
public Resource getResource(ResourceRequest request) 

Puede analizar esta anotación estandarizada con un ContainerResponseFilter

@Provider
public class StatusFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
        if (responseContext.getStatus() == 200) {
            for (final var annotation : responseContext.getEntityAnnotations()) {
                if (annotation instanceof APIResponse response) {
                    final var rawCode = response.responseCode();
                    final var statusCode = Integer.parseInt(rawCode);

                    responseContext.setStatus(statusCode);
                }
            }
        }
    }

}

Una advertencia ocurre cuando pones múltiples anotaciones en tu método como

@APIResponse(responseCode = "201", description = "first use case")
@APIResponse(responseCode = "204", description = "because you can")
public Resource getResource(ResourceRequest request) 

-1

Estoy usando jersey 2.0 con lectores y escritores de mensajes. Tenía mi tipo de retorno de método como una entidad específica que también se usó 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é solo la implementación del escritor y aún funcionó.

public Response getSkuData()
...
return Response.status(Response.Status.FORBIDDEN).entity(dfCoreResultListDTO).build();
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.