Comment répondre avec une erreur HTTP 400 dans une méthode Spring MVC @ResponseBody renvoyant une chaîne?

389

J'utilise Spring MVC pour une API JSON simple, avec @ResponseBodyune approche basée comme celle-ci. (J'ai déjà une couche de service qui produit directement JSON.)

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

La question est, dans le scénario donné, quelle est la façon la plus simple et la plus propre de répondre avec une erreur HTTP 400 ?

J'ai rencontré des approches comme:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

... mais je ne peux pas l'utiliser ici car le type de retour de ma méthode est String, pas ResponseEntity.

Jonik
la source

Réponses:

624

changez votre type de retour en ResponseEntity<>, puis vous pouvez utiliser ci-dessous pour 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

et pour une demande correcte

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

MISE À JOUR 1

après le printemps 4.1, il existe des méthodes d'aide dans ResponseEntity pouvant être utilisées comme

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

et

return ResponseEntity.ok(json);
Bassem Reda Zohdy
la source
Ah, vous pouvez donc utiliser ResponseEntitycomme ça aussi. Cela fonctionne bien et n'est qu'une simple modification du code d'origine - merci!
Jonik
vous êtes les bienvenus chaque fois que vous pouvez ajouter un en-tête personnalisé aussi vérifiez tous les constructeurs de ResponseEntity
Bassem Reda Zohdy
7
Et si vous passiez autre chose qu'une chaîne? Comme dans un POJO ou un autre objet?
mrshickadance
11
ce sera 'ResponseEntity <YourClass>'
Bassem Reda Zohdy
5
En utilisant cette approche, vous n'avez plus besoin d'annotation
@ResponseBody
108

Quelque chose comme ça devrait fonctionner, je ne sais pas s'il existe ou non un moyen plus simple:

@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;
}
gerbeur
la source
5
Merci! Cela fonctionne et est assez simple aussi. (Dans ce cas , il pourrait être encore simplifiée en supprimant la non utilisée bodyet requestparams.)
Jonik
54

Pas nécessairement la façon la plus compacte de faire cela, mais l'OMI assez propre

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

Modifier, vous pouvez utiliser @ResponseBody dans la méthode du gestionnaire d'exceptions si vous utilisez Spring 3.1+, sinon utilisez un ModelAndViewou quelque chose.

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

Zutty
la source
1
Désolé, cela ne semble pas fonctionner. Il produit une erreur de serveur HTTP 500 avec une longue trace de pile dans les journaux: 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 representationmanque-t-il quelque chose dans la réponse?
Jonik
De plus, je n'ai pas bien compris l'intérêt de définir un autre type personnalisé (MyError). Est-ce que c'est nécessaire? J'utilise la dernière version Spring (3.2.2).
Jonik
1
Ça marche pour moi. J'utilise à la javax.validation.ValidationExceptionplace. (Printemps 3.1.4)
Jerry Chen
Ceci est très utile dans les situations où vous avez une couche intermédiaire entre votre service et le client où la couche intermédiaire a ses propres capacités de gestion des erreurs. Merci pour cet exemple @Zutty
StormeHawke
Cela devrait être la réponse acceptée, car elle déplace le code de gestion des exceptions du flux normal et cache HttpServlet *
lilalinux
48

Je changerais légèrement la mise en œuvre:

Tout d'abord, je crée un UnknownMatchException:

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

Notez l'utilisation de @ResponseStatus , qui sera reconnu par Spring's ResponseStatusExceptionResolver. Si l'exception est levée, elle créera une réponse avec l'état de réponse correspondant. (J'ai également pris la liberté de changer le code d'état 404 - Not Foundauquel je trouve plus approprié pour ce cas d'utilisation, mais vous pouvez vous y tenir HttpStatus.BAD_REQUESTsi vous le souhaitez.)


Ensuite, je changerais le MatchServicepour avoir la signature suivante:

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

Enfin, je voudrais mettre à jour le contrôleur et déléguer à Spring MappingJackson2HttpMessageConverterpour gérer automatiquement la sérialisation JSON (il est ajouté par défaut si vous ajoutez Jackson au chemin de classe et ajoutez soit @EnableWebMvcou <mvc:annotation-driven />à votre configuration, voir les documents de référence ):

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

Remarque, il est très courant de séparer les objets de domaine des objets de vue ou des objets DTO. Cela peut facilement être réalisé en ajoutant une petite fabrique DTO qui renvoie l'objet JSON sérialisable:

@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
la source
J'ai 500 et je me connecte: 28 août 2015 17:23:31 org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERE: Une erreur s'est produite lors de la gestion des erreurs, abandonnez! org.apache.cxf.interceptor.Fault
rasoir
Solution parfaite, je veux seulement ajouter que j'espère que le DTO est une composition Matchet un autre objet.
Marco Sulla
32

Voici une approche différente. Créez une Exceptionannotation personnalisée avec @ResponseStatus, comme la suivante.

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

    public NotFoundException() {
    }
}

Et jetez-le en cas de besoin.

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

Consultez la documentation Spring ici: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .

danidemi
la source
Cette approche vous permet de terminer l'exécution où que vous soyez dans le stacktrace sans avoir à retourner une "valeur spéciale" qui devrait spécifier le code d'état HTTP que vous souhaitez retourner.
Muhammad Gelbana
21

Comme mentionné dans certaines réponses, il est possible de créer une classe d'exception pour chaque statut HTTP que vous souhaitez renvoyer. Je n'aime pas l'idée de devoir créer une classe par statut pour chaque projet. Voici ce que j'ai trouvé à la place.

  • Créer une exception générique qui accepte un statut HTTP
  • Créer un gestionnaire d'exceptions Controller Advice

Passons au code

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

Ensuite, je crée une classe de conseils de contrôleur

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

Pour l'utiliser

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

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

Norris
la source
Très bonne méthode .. Au lieu d'une simple chaîne, je préfère renvoyer un jSON avec errorCode et des champs de message ..
İsmail Yavuz
1
Cela devrait être la bonne réponse, un gestionnaire d'exceptions générique et global avec un code d'état et un message personnalisés: D
Pedro Silva
10

J'utilise ceci dans mon application Spring Boot

@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
la source
9

Le moyen le plus simple est de lancer un 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
la source
3
Meilleure réponse: pas besoin de changer le type de retour et pas besoin de créer votre propre exception. De plus, ResponseStatusException permet d'ajouter un message de motif si nécessaire.
Migs
Il est important de noter que ResponseStatusException n'est disponible que dans la version Spring 5+
Ethan Conner
2

Avec Spring Boot, je ne sais pas vraiment pourquoi cela était nécessaire (j'ai eu le /errorrepli même si cela a @ResponseBodyété défini sur un @ExceptionHandler), mais ce qui suit en soi n'a pas fonctionné:

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

Il a toujours levé une exception, apparemment parce qu'aucun type de média productible n'a été défini comme attribut de demande:

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

Je les ai donc ajoutés.

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

Et cela m'a permis d'avoir un "type de support compatible pris en charge", mais cela n'a toujours pas fonctionné, car mon ErrorMessageétait défectueux:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper ne l'a pas traité comme "convertible", j'ai donc dû ajouter des getters / setters, et j'ai également ajouté des @JsonPropertyannotations

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

Ensuite, j'ai reçu mon message comme prévu

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

Vous pouvez également throw new HttpMessageNotReadableException("error description")profiter de la gestion des erreurs par défaut de Spring .

Cependant, comme c'est le cas avec ces erreurs par défaut, aucun corps de réponse ne sera défini.

Je les trouve utiles lorsque je rejette des demandes qui ne pouvaient raisonnablement avoir été fabriquées qu'à la main, indiquant potentiellement une intention malveillante, car elles masquent le fait que la demande a été rejetée sur la base d'une validation personnalisée plus approfondie et de ses critères.

Hth, dtk

dtk
la source
HttpMessageNotReadableException("error description")est obsolète.
Kuba Šimonovský
0

Une autre approche consiste à utiliser @ExceptionHandleravec @ControllerAdvicepour centraliser tous vos gestionnaires dans la même classe, sinon vous devez placer les méthodes de gestionnaire dans chaque contrôleur avec lequel vous souhaitez gérer une exception.

Votre classe de gestionnaire:

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

Votre exception personnalisée:

public class MyBadRequestException extends RuntimeException {

  private String description;

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

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

Vous pouvez maintenant lever des exceptions à partir de n'importe lequel de vos contrôleurs, et vous pouvez définir d'autres gestionnaires dans votre classe de conseils.

Gonzalo
la source
-1

Je pense que ce fil a en fait la solution la plus simple et la plus propre, qui ne sacrifie pas les outils de martialisation JSON fournis par Spring:

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

Ryan S
la source