Como retornar o código de status JSON e HTTP juntos?

248

Estou escrevendo um aplicativo Web REST (NetBeans 6.9, JAX-RS, TopLink Essentials) e tentando retornar o código de status JSON e HTTP. Eu tenho código pronto e funcionando que retorna JSON quando o método HTTP GET é chamado do cliente. Essencialmente:

@Path("get/id")
@GET
@Produces("application/json")
public M_機械 getMachineToUpdate(@PathParam("id") String id) {

    // some code to return JSON ...

    return myJson;
}

Mas também quero retornar um código de status HTTP (500, 200, 204 etc.) junto com os dados JSON.

Eu tentei usar HttpServletResponse:

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

Mas isso fez o navegador pensar que é um 500 "real"; portanto, a página da Web de saída era uma página de erro HTTP 500 normal.

Desejo retornar um código de status HTTP para que meu JavaScript do lado do cliente possa lidar com alguma lógica dependendo dele (por exemplo, exibir o código de erro e a mensagem em uma página HTML). Isso é possível ou os códigos de status HTTP não devem ser usados ​​para isso?

Miau
fonte
2
Qual é a diferença entre 500 que você deseja (irreal? :)) e 500 reais?
Navalha #
@razor Aqui verdadeira 500 meios uma página de erro HTML em vez de resposta JSON
Nupur
O navegador da web não foi projetado para funcionar com JSON, mas com páginas HTML; portanto, se você responder com 500 (e até mesmo algum corpo de mensagem), o navegador poderá mostrar apenas uma mensagem de erro (depende realmente da implementação do navegador), apenas porque é útil para um usuário normal.
Navalha #

Respostas:

347

Aqui está um exemplo:

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

Dê uma olhada na classe Response .

Observe que você sempre deve especificar um tipo de conteúdo, especialmente se estiver passando vários tipos de conteúdo, mas se todas as mensagens forem representadas como JSON, você pode apenas anotar o método com @Produces("application/json")

hisdrewness
fonte
12
Funciona, mas o que eu não gosto no valor de retorno Response é que, na minha opinião, polui seu código, especialmente em relação a qualquer cliente que esteja tentando usá-lo. Se você fornecer uma interface retornando uma resposta a terceiros, ele não saberá que tipo você realmente está retornando. Primavera torna mais clara, com uma anotação, muito útil se você sempre retornar um código de status (ou seja, HTTP 204)
Guido
19
Tornar essa classe genérica (Resposta <T>) seria uma melhoria interessante para o jax-rs, para ter as vantagens de ambas as alternativas.
Guido
41
Não há necessidade de converter a entidade em json de alguma forma. Você pode executar o return Response.status(Response.Status.Forbidden).entity(myPOJO).build();Works como se fosse return myPOJO;, mas com configurações adicionais do código de status HTTP.
kratenko
1
Eu acho que separar a lógica de negócios em uma classe de serviço separada funciona bem. O nó de extremidade usa Response como tipo de retorno e seus métodos são principalmente apenas chamadas para os métodos de serviço, além das anotações de caminho e parâmetros. Ele separa claramente a lógica do mapeamento de tipo de URL / conteúdo (onde a borracha atinge a estrada, por assim dizer).
Stijn de Witt
na verdade, pode-se retornar o objeto que não está quebrando para a resposta.
ses
191

Existem vários casos de uso para definir códigos de status HTTP em um serviço da Web REST, e pelo menos um não foi suficientemente documentado nas respostas existentes (por exemplo, quando você está usando a serialização JSON / XML auto-mágica usando JAXB e deseja retornar um objeto a ser serializado, mas também um código de status diferente do padrão 200).

Então, deixe-me tentar enumerar os diferentes casos de uso e as soluções para cada um:

1. Código de erro (500, 404, ...)

O caso de uso mais comum em que você deseja retornar um código de status diferente 200 OKdaquele em que ocorre um erro.

Por exemplo:

  • uma entidade é solicitada, mas ela não existe (404)
  • a solicitação está semanticamente incorreta (400)
  • o usuário não está autorizado (401)
  • há um problema com a conexão com o banco de dados (500)
  • etc ..

a) Lance uma exceção

Nesse caso, acho que a maneira mais limpa de lidar com o problema é lançar uma exceção. Essa exceção será tratada por um ExceptionMapper, que converterá a exceção em uma resposta com o código de erro apropriado.

Você pode usar o padrão ExceptionMapperque vem pré-configurado com Jersey (e acho que é o mesmo com outras implementações) e lançar qualquer uma das subclasses existentes de javax.ws.rs.WebApplicationException. Estes são tipos de exceção predefinidos, pré-mapeados para diferentes códigos de erro, por exemplo:

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

Etc. Você pode encontrar a lista aqui: API

Como alternativa, você pode definir suas próprias exceções e ExceptionMapperclasses personalizadas e adicionar esses mapeadores a Jersey pela média da @Provideranotação ( fonte deste exemplo ):

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

Fornecedor :

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

Nota: você também pode escrever ExceptionMappers para os tipos de exceção existentes que você usa.

b) Use o construtor Response

Outra maneira de configurar um código de status é usar um Responseconstrutor para construir uma resposta com o código pretendido.

Nesse caso, o tipo de retorno do seu método deve ser javax.ws.rs.core.Response. Isso é descrito em várias outras respostas, como a resposta aceita pela desistência e se parece com isso:

@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. Sucesso, mas não 200

Outro caso em que você deseja definir o status de retorno é quando a operação foi bem-sucedida, mas você deseja retornar um código de sucesso diferente de 200, juntamente com o conteúdo que você retorna no corpo.

Um caso de uso frequente é quando você cria uma nova entidade ( POSTsolicitação) e deseja retornar informações sobre essa nova entidade ou talvez a própria entidade, junto com um 201 Createdcódigo de status.

Uma abordagem é usar o objeto de resposta como descrito acima e definir o corpo da solicitação. No entanto, ao fazer isso, você perde a capacidade de usar a serialização automática para XML ou JSON fornecida pelo JAXB.

Este é o método original que retorna um objeto de entidade que será serializado em JSON por JAXB:

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

Isso retornará uma representação JSON do usuário recém-criado, mas o status de retorno será 200, não 201.

Agora, o problema é que, se eu quiser usar o Responseconstrutor para definir o código de retorno, tenho que retornar um Responseobjeto no meu método. Como ainda retorno o Userobjeto a ser serializado?

a) Defina o código na resposta do servlet

Uma abordagem para resolver isso é obter um objeto de solicitação de servlet e definir o código de resposta manualmente, como demonstrado na resposta 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;
}

O método ainda retorna um objeto de entidade e o código de status será 201.

Observe que, para fazê-lo funcionar, tive que liberar a resposta. Esse é um ressurgimento desagradável do código da API do Servlet de baixo nível em nosso bom recurso JAX_RS e, muito pior, faz com que os cabeçalhos não sejam modificáveis ​​depois disso, porque eles já foram enviados pela conexão.

b) Use o objeto de resposta com a entidade

A melhor solução, nesse caso, é usar o objeto Response e configurar a entidade para ser serializada nesse objeto de resposta. Seria bom tornar o objeto Response genérico para indicar o tipo de entidade de carga útil nesse caso, mas atualmente não é o caso.

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

Nesse caso, usamos o método criado da classe do construtor Response para definir o código de status como 201. Passamos o objeto de entidade (usuário) para a resposta por meio do método entity ().

O resultado é que o código HTTP é 401 como desejávamos, e o corpo da resposta é exatamente o mesmo JSON que tínhamos antes, quando retornamos o objeto Usuário. Ele também adiciona um cabeçalho de localização.

A classe Response possui vários métodos do construtor para diferentes status (stati?), Como:

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

NB: o objeto hateoas é uma classe auxiliar que desenvolvi para ajudar a gerar URIs de recursos. Você precisará criar seu próprio mecanismo aqui;)

É sobre isso.

Espero que esta longa resposta ajude alguém :)

Pierre Henry
fonte
Gostaria de saber se existe uma maneira limpa de retornar o próprio objeto de dados em vez da resposta. O flushfato é sujo.
AlikElzin-kilaka
1
Apenas uma irritação minha: 401 não significa que o usuário não está autorizado. Isso significa que o cliente não está autorizado, porque o servidor não sabe quem você é. Se um usuário registrado / reconhecido de outra forma não tiver permissão para executar uma determinada ação, o código de resposta correto será 403 Proibido.
Demonblack
69

A resposta por herdrewness funcionará, mas modifica toda a abordagem para permitir que um provedor como o Jackson + JAXB converta automaticamente o objeto retornado em algum formato de saída como JSON. Inspirado em uma postagem do Apache CXF (que usa uma classe específica do CXF), encontrei uma maneira de definir o código de resposta que deve funcionar em qualquer implementação do JAX-RS: injete um contexto HttpServletResponse e defina manualmente o código de resposta. Por exemplo, aqui está como definir o código de resposta para CREATEDquando apropriado.

@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
}

Melhoria: Depois de encontrar outra resposta relacionada , aprendi que é possível injetar a HttpServletResponsevariável como membro, mesmo para a classe de serviço singleton (pelo menos em RESTEasy) !! Essa é uma abordagem muito melhor do que poluir a API com detalhes de implementação. Seria assim:

@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
}
Garret Wilson
fonte
Você pode realmente combinar as abordagens: anote o método com @Producese não especifique o tipo de mídia na final Response.ok, e você obterá seu objeto de retorno corretamente serializado por JAXB no tipo de mídia apropriado para corresponder à solicitação. (Eu apenas tentei isso com um único método que poderia retornar XML ou JSON: o método em si não precisa mencionar qualquer um, exceto na @Producesanotação.)
Royston Shufflebotham
Você está certo, Garret. Meu exemplo foi mais uma ilustração da ênfase em fornecer um tipo de conteúdo. Nossas abordagens são semelhantes, mas a idéia de usar um MessageBodyWritere Providerpermite a negociação de conteúdo implícito, embora pareça o seu exemplo está faltando algum código. Aqui está outra resposta que forneci que ilustra isso: stackoverflow.com/questions/5161466/… #
hisdrewness
8
Não consigo substituir o código de status response.setStatus(). A única maneira de enviar, por exemplo, uma resposta 404 Não encontrada, é definir o código de status da resposta response.setStatus(404)e fechar o fluxo de saída response.getOutputStream().close()para que o JAX-RS não possa redefinir meu status.
Rob Juurlink
2
Consegui usar essa abordagem para definir um código 201, mas tive que adicionar um bloco try-catch response.flushBuffer()para evitar que a estrutura substituísse meu código de resposta. Não é muito limpo.
31716 Pierre Pierre
1
@RobJuurlink, se você quiser retornar um a 404 Not Found, pode ser mais fácil usá-lo throw new NotFoundException().
Garret Wilson
34

Se você deseja manter sua camada de recursos limpa de Responseobjetos, recomendo que você use @NameBindinge ligue as implementações de ContainerResponseFilter.

Aqui está o conteúdo da anotação:

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

Aqui está a carne do 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;
        }
      }
    }
  }
}

E então a implementação em seu recurso simplesmente se torna:

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;
  }
}
Nthalk
fonte
Mantém a API limpa e agradável. Seria possível parametrizar sua anotação como @Status (código = 205) e fazer com que o interceptador substituísse o código pelo que você especificar? Eu acho que isso basicamente daria uma anotação para substituir os códigos sempre que você precisar.
precisa saber é o seguinte
@ user2800708, eu já fiz isso para o meu código local, atualizei a resposta conforme sugerido.
Nthalk #
Bom obrigado. Com esse e alguns outros truques, agora eu basicamente sou capaz de limpar as APIs REST no meu código, para que ele se adapte a uma interface Java simples, sem mencionar o REST; é apenas mais um mecanismo de RMI.
precisa saber é o seguinte
6
Em vez de fazer o loop para anotações no StatusFilter, você pode anotar o filtro com @ Status, além de @ Provider. Em seguida, o filtro será chamado apenas nos recursos anotados com @ Status. Este é o propósito da @ NameBinding
trevorism
1
Bom texto explicativo @trevorism. Há um efeito colateral não tão agradável de anotar StatusFiltercom @Status: você quer necessidade de fornecer um padrão para o de anotação valuecampo, ou declarar um quando anotando a classe (ex: @Status(200)). Obviamente, isso não é o ideal.
Phil
6

Caso deseje alterar o código de status devido a uma exceção, com o JAX-RS 2.0, você pode implementar um ExceptionMapper como este. Isso lida com esse tipo de exceção para todo o aplicativo.

@Provider
public class UnauthorizedExceptionMapper implements ExceptionMapper<EJBAccessException> {

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

}
ercalamar
fonte
6

Se o seu WS-RS precisar gerar um erro, por que não usar apenas o 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;
}
Ariel
fonte
4
Na minha opinião, WebApplicationExceptions não são adequados para erros do lado do cliente porque eles lançam grandes rastreamentos de pilha. Os erros do cliente não devem lançar rastreamentos de pilha do lado do servidor e poluir o log com ele.
Rob Juurlink
5

O JAX-RS possui suporte para códigos HTTP padrão / customizados. Consulte ResponseBuilder e ResponseStatus, por exemplo:

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

Lembre-se de que as informações JSON são mais sobre os dados associados ao recurso / aplicativo. Os códigos HTTP são mais sobre o status da operação CRUD que está sendo solicitada. (pelo menos é assim que deveria ser nos sistemas REST-ful)

kvista
fonte
a ligação é interrompida
Umpa
5

Achei muito útil criar também uma mensagem json com código repetido, assim:

@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();
    }
}
Matriz
fonte
4

Veja o exemplo aqui, ele ilustra melhor o problema e como ele foi resolvido na versão mais recente (2.3.1) do Jersey.

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

Basicamente, envolve definir uma exceção personalizada e manter o tipo de retorno como a entidade. Quando houver um erro, a exceção será lançada; caso contrário, você retornará o POJO.

annouk
fonte
Gostaria de acrescentar que o exemplo de interesse é aquele em que eles definem sua própria classe de exceção e criam uma Responsenela. Basta procurar a CustomNotFoundExceptionturma e talvez copiá-la para sua postagem.
JBert
Eu uso essa abordagem para erros e eu gosto. Mas não é aplicável a códigos de sucesso (diferentes de 200), como '201 criado'.
Pierre Henry
3

Não estou usando JAX-RS, mas tenho um cenário semelhante em que uso:

response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
Cápsulas
fonte
Faz para mim usando o Spring MVC, mas há uma maneira fácil de descobrir!
Caps
1

Além disso, observe que, por padrão, Jersey substituirá o corpo da resposta no caso de um código http 400 ou mais.

Para obter sua entidade especificada como o corpo da resposta, tente adicionar o seguinte init-param ao seu Jersey no seu arquivo de configuração 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>
Maxime T
fonte
0

O código a seguir funcionou para mim. Injetando o messageContext via setter anotado e definindo o código de status no meu método "add".

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);
    }
}
JBernhardt
fonte
0

Expandindo a resposta da Nthalk com o Microprofile OpenAPI, você pode alinhar o código de retorno à sua documentação usando a anotação @APIResponse .

Isso permite marcar um método JAX-RS como

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

Você pode analisar essa anotação padronizada com um 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);
                }
            }
        }
    }

}

Uma ressalva ocorre quando você coloca várias anotações no seu método, como

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

Estou usando o jersey 2.0 com leitores e escritores do corpo da mensagem. Eu tive meu tipo de retorno de método como uma entidade específica que também foi usada na implementação do gravador do corpo da mensagem e estava retornando o mesmo pojo, um SkuListDTO. @GET @Consumes ({"application / xml", "application / json"}) @Produces ({"application / xml", "application / json"}) @Path ("/ skuResync")

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

tudo que mudei foi isso, deixei a implementação do gravador sozinha e ainda funcionava.

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