Como responder com erro HTTP 400 em um método Spring MVC @ResponseBody retornando String?

389

Estou usando o Spring MVC para uma API JSON simples, com @ResponseBodyabordagem baseada na seguinte. (Eu já tenho uma camada de serviço produzindo JSON diretamente.)

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId) {
    String json = matchService.getMatchJson(matchId);
    if (json == null) {
        // TODO: how to respond with e.g. 400 "bad request"?
    }
    return json;
}

A pergunta é, no cenário fornecido, qual é a maneira mais simples e limpa de responder com um erro do HTTP 400 ?

Me deparei com abordagens como:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

... mas não posso usá-lo aqui, pois o tipo de retorno do meu método é String, não ResponseEntity.

Jonik
fonte

Respostas:

624

altere seu tipo de retorno para ResponseEntity<>, então você pode usar abaixo por 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

e para solicitação correta

return new ResponseEntity<>(json,HttpStatus.OK);

ATUALIZAÇÃO 1

Após a primavera 4.1, existem métodos auxiliares no ResponseEntity que podem ser usados ​​como

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);

e

return ResponseEntity.ok(json);
Bassem Reda Zohdy
fonte
Ah, então você pode usar ResponseEntityassim também. Isso funciona bem e é apenas uma simples alteração no código original - obrigado!
Jonik
Você está convidado a qualquer momento você pode adicionar cabeçalho personalizado também verificar todos os construtores de ResponseEntity
Bassem Reda Zohdy
7
E se você estiver passando algo diferente de uma corda de volta? Como em um POJO ou outro objeto?
mrshickadance
11
será 'ResponseEntity <YourClass>'
Bassem Reda Zohdy
5
Usando essa abordagem, você não precisa de anotação @ResponseBody mais
Lu55
108

Algo assim deve funcionar, não tenho certeza se existe ou não uma maneira mais simples:

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId, @RequestBody String body,
            HttpServletRequest request, HttpServletResponse response) {
    String json = matchService.getMatchJson(matchId);
    if (json == null) {
        response.setStatus( HttpServletResponse.SC_BAD_REQUEST  );
    }
    return json;
}
empilhador
fonte
5
Obrigado! Isso funciona e também é bastante simples. (Neste caso, poderia ser ainda mais simplificado através da remoção do não utilizado bodye requestparâmetros.)
Jonik
54

Não necessariamente a maneira mais compacta de fazer isso, mas IMO bastante limpo

if(json == null) {
    throw new BadThingException();
}
...

@ExceptionHandler(BadThingException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public @ResponseBody MyError handleException(BadThingException e) {
    return new MyError("That doesnt work");
}

Na edição, você pode usar @ResponseBody no método manipulador de exceções, se estiver usando o Spring 3.1+, caso contrário, use a ModelAndViewou algo assim.

https://jira.springsource.org/browse/SPR-6902

Zutty
fonte
11
Desculpe, isso não parece funcionar. Produz HTTP 500 "erro do servidor" com rastreamento de pilha longa nos logs: ERROR org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - Failed to invoke @ExceptionHandler method: public controller.TestController$MyError controller.TestController.handleException(controller.TestController$BadThingException) org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representationHá algo faltando na resposta?
Jonik
Além disso, não entendi completamente o objetivo de definir outro tipo personalizado (MyError). Isso é necessário? Estou usando o Spring mais recente (3.2.2).
Jonik
11
Funciona para mim. Eu uso em javax.validation.ValidationExceptionvez disso. (Spring 3.1.4)
Jerry Chen
Isso é bastante útil em situações em que você possui uma camada intermediária entre o serviço e o cliente, onde a camada intermediária possui seus próprios recursos de tratamento de erros. Obrigado por este exemplo @Zutty
StormeHawke
Esta deve ser a resposta aceita, como ele se move o código de tratamento de exceções fora do fluxo normal e peles HttpServlet *
lilalinux
48

Eu mudaria um pouco a implementação:

Primeiro, eu crio um UnknownMatchException:

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UnknownMatchException extends RuntimeException {
    public UnknownMatchException(String matchId) {
        super("Unknown match: " + matchId);
    }
}

Observe o uso do @ResponseStatus , que será reconhecido pelo Spring ResponseStatusExceptionResolver. Se a exceção for lançada, ela criará uma resposta com o status de resposta correspondente. (Também tomei a liberdade de alterar o código de status ao 404 - Not Foundqual acho mais apropriado para este caso de uso, mas você pode continuar HttpStatus.BAD_REQUESTse quiser.)


Em seguida, eu mudaria MatchServicepara ter a seguinte assinatura:

interface MatchService {
    public Match findMatch(String matchId);
}

Finalmente, eu atualizaria o controlador e o delegaria ao Spring MappingJackson2HttpMessageConverterpara lidar com a serialização JSON automaticamente (ela será adicionada por padrão se você adicionar Jackson ao caminho de classe e adicionar uma @EnableWebMvcou <mvc:annotation-driven />sua configuração, consulte os documentos de referência ):

@RequestMapping(value = "/matches/{matchId}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Match match(@PathVariable String matchId) {
    // throws an UnknownMatchException if the matchId is not known 
    return matchService.findMatch(matchId);
}

Observe que é muito comum separar os objetos de domínio dos objetos de exibição ou DTO. Isso pode ser facilmente alcançado adicionando uma pequena fábrica de DTO que retorna o objeto JSON serializável:

@RequestMapping(value = "/matches/{matchId}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public MatchDTO match(@PathVariable String matchId) {
    Match match = matchService.findMatch(matchId);
    return MatchDtoFactory.createDTO(match);
}
Matsev
fonte
Eu tenho 500 e eu registra: 28 de maio de 2015 17:23:31 PM org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERO: Ocorreu um erro durante o tratamento de erros, desista! org.apache.cxf.interceptor.Fault
navalha
Solução perfeita, quero apenas acrescentar que espero que o DTO seja uma composição de Matche algum outro objeto.
Marco Sulla
32

Aqui está uma abordagem diferente. Crie um personalizado Exceptionanotado com @ResponseStatus, como o seguinte.

@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Not Found")
public class NotFoundException extends Exception {

    public NotFoundException() {
    }
}

E jogue-o quando necessário.

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId) {
    String json = matchService.getMatchJson(matchId);
    if (json == null) {
        throw new NotFoundException();
    }
    return json;
}

Confira a documentação do Spring aqui: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .

danidemi
fonte
Essa abordagem permite que você encerre a execução onde quer que esteja no stacktrace sem precisar retornar um "valor especial" que deve especificar o código de status HTTP que você deseja retornar.
Muhammad Gelbana 28/11
21

Conforme mencionado em algumas respostas, existe a capacidade de criar uma classe de exceção para cada status HTTP que você deseja retornar. Não gosto da ideia de ter que criar uma classe por status para cada projeto. Aqui está o que eu inventei.

  • Crie uma exceção genérica que aceite um status HTTP
  • Criar um manipulador de exceções do Controller Advice

Vamos ao código

package com.javaninja.cam.exception;

import org.springframework.http.HttpStatus;


/**
 * The exception used to return a status and a message to the calling system.
 * @author norrisshelton
 */
@SuppressWarnings("ClassWithoutNoArgConstructor")
public class ResourceException extends RuntimeException {

    private HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;

    /**
     * Gets the HTTP status code to be returned to the calling system.
     * @return http status code.  Defaults to HttpStatus.INTERNAL_SERVER_ERROR (500).
     * @see HttpStatus
     */
    public HttpStatus getHttpStatus() {
        return httpStatus;
    }

    /**
     * Constructs a new runtime exception with the specified HttpStatus code and detail message.
     * The cause is not initialized, and may subsequently be initialized by a call to {@link #initCause}.
     * @param httpStatus the http status.  The detail message is saved for later retrieval by the {@link
     *                   #getHttpStatus()} method.
     * @param message    the detail message. The detail message is saved for later retrieval by the {@link
     *                   #getMessage()} method.
     * @see HttpStatus
     */
    public ResourceException(HttpStatus httpStatus, String message) {
        super(message);
        this.httpStatus = httpStatus;
    }
}

Então eu crio uma classe de conselho de controlador

package com.javaninja.cam.spring;


import com.javaninja.cam.exception.ResourceException;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;


/**
 * Exception handler advice class for all SpringMVC controllers.
 * @author norrisshelton
 * @see org.springframework.web.bind.annotation.ControllerAdvice
 */
@org.springframework.web.bind.annotation.ControllerAdvice
public class ControllerAdvice {

    /**
     * Handles ResourceExceptions for the SpringMVC controllers.
     * @param e SpringMVC controller exception.
     * @return http response entity
     * @see ExceptionHandler
     */
    @ExceptionHandler(ResourceException.class)
    public ResponseEntity handleException(ResourceException e) {
        return ResponseEntity.status(e.getHttpStatus()).body(e.getMessage());
    }
}

Para usá-lo

throw new ResourceException(HttpStatus.BAD_REQUEST, "My message");

http://javaninja.net/2016/06/throwing-exceptions-messages-spring-mvc-controller/

Norris
fonte
Método muito bom .. Em vez de uma string simples, prefiro retornar um jSON com errorCode e campos de mensagem .. #
Ymail Yavuz
11
Esta deve ser a resposta correta, um manipulador de exceção genérica e global com código de status personalizado e mensagem: D
Pedro Silva
10

Estou usando isso no meu aplicativo de inicialização da primavera

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public ResponseEntity<?> match(@PathVariable String matchId, @RequestBody String body,
            HttpServletRequest request, HttpServletResponse response) {

    Product p;
    try {
      p = service.getProduct(request.getProductId());
    } catch(Exception ex) {
       return new ResponseEntity<String>(HttpStatus.BAD_REQUEST);
    }

    return new ResponseEntity(p, HttpStatus.OK);
}
Aamir Faried
fonte
9

A maneira mais fácil é lançar um ResponseStatusException

    @RequestMapping(value = "/matches/{matchId}", produces = "application/json")
    @ResponseBody
    public String match(@PathVariable String matchId, @RequestBody String body) {
        String json = matchService.getMatchJson(matchId);
        if (json == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }
        return json;
    }
MevlütÖzdemir
fonte
3
Melhor resposta: não é necessário alterar o tipo de retorno nem criar sua própria exceção. Além disso, o ResponseStatusException permite adicionar uma mensagem de razão, se necessário.
Migs
Importante observar que o ResponseStatusException está disponível apenas na versão 5+ do Spring
Ethan Conner
2

Com o Spring Boot, não sei ao certo por que isso foi necessário (recebi o /errorfallback, embora tenha @ResponseBodysido definido em um @ExceptionHandler), mas o seguinte por si só não funcionou:

@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorMessage handleIllegalArguments(HttpServletRequest httpServletRequest, IllegalArgumentException e) {
    log.error("Illegal arguments received.", e);
    ErrorMessage errorMessage = new ErrorMessage();
    errorMessage.code = 400;
    errorMessage.message = e.getMessage();
    return errorMessage;
}

Ainda gerou uma exceção, aparentemente porque nenhum tipo de mídia produtável foi definido como um atributo de solicitação:

// AbstractMessageConverterMethodProcessor
@SuppressWarnings("unchecked")
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
        ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    Class<?> valueType = getReturnValueType(value, returnType);
    Type declaredType = getGenericType(returnType);
    HttpServletRequest request = inputMessage.getServletRequest();
    List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
    List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
if (value != null && producibleMediaTypes.isEmpty()) {
        throw new IllegalArgumentException("No converter found for return value of type: " + valueType);   // <-- throws
    }

// ....

@SuppressWarnings("unchecked")
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
    Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    if (!CollectionUtils.isEmpty(mediaTypes)) {
        return new ArrayList<MediaType>(mediaTypes);

Então eu os adicionei.

@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorMessage handleIllegalArguments(HttpServletRequest httpServletRequest, IllegalArgumentException e) {
    Set<MediaType> mediaTypes = new HashSet<>();
    mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
    httpServletRequest.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
    log.error("Illegal arguments received.", e);
    ErrorMessage errorMessage = new ErrorMessage();
    errorMessage.code = 400;
    errorMessage.message = e.getMessage();
    return errorMessage;
}

E isso me levou a ter um "tipo de mídia compatível compatível", mas ainda assim não funcionou, porque ErrorMessageestava com defeito:

public class ErrorMessage {
    int code;

    String message;
}

O JacksonMapper não o tratou como "conversível", então tive que adicionar getters / setters e também adicionei @JsonPropertyanotação

public class ErrorMessage {
    @JsonProperty("code")
    private int code;

    @JsonProperty("message")
    private String message;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

Então recebi minha mensagem como pretendido

{"code":400,"message":"An \"url\" parameter must be defined."}
EpicPandaForce
fonte
0

Você também pode throw new HttpMessageNotReadableException("error description")se beneficiar do tratamento de erros padrão do Spring .

No entanto, assim como ocorre com esses erros padrão, nenhum corpo de resposta será definido.

Acho isso útil ao rejeitar solicitações que poderiam razoavelmente ter sido criadas manualmente, indicando potencialmente uma intenção malévola, pois ocultam o fato de que a solicitação foi rejeitada com base em uma validação personalizada mais profunda e em seus critérios.

Hth, dtk

dtk
fonte
HttpMessageNotReadableException("error description")está obsoleto.
Kuba Šimonovský
0

Outra abordagem é usar @ExceptionHandlercom @ControllerAdvicepara centralizar todos os seus manipuladores na mesma classe; caso contrário, você deve colocar os métodos do manipulador em todos os controladores em que deseja gerenciar uma exceção.

Sua classe de manipulador:

@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler(MyBadRequestException.class)
  public ResponseEntity<MyError> handleException(MyBadRequestException e) {
    return ResponseEntity
        .badRequest()
        .body(new MyError(HttpStatus.BAD_REQUEST, e.getDescription()));
  }
}

Sua exceção personalizada:

public class MyBadRequestException extends RuntimeException {

  private String description;

  public MyBadRequestException(String description) {
    this.description = description;
  }

  public String getDescription() {
    return this.description;
  }
}

Agora você pode lançar exceções em qualquer um dos seus controladores e definir outros manipuladores dentro da classe de aconselhamento.

Gonzalo
fonte
-1

Eu acho que esse segmento realmente tem a solução mais fácil e limpa, que não sacrifica as ferramentas de marcial JSON fornecidas pelo Spring:

https://stackoverflow.com/a/16986372/1278921

Ryan S
fonte