web-dev-qa-db-fra.com

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

J'utilise Spring MVC pour une simple API JSON, avec une approche basée sur @ResponseBody comme ci-dessous. (J'ai déjà une couche de service produisant directement du 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 manière la plus simple et la plus propre de répondre avec une erreur HTTP 4?

J'ai rencontré des approches telles que:

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.

351
Jonik

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

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

et pour demande correcte

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

PDATE 1

après le printemps 4.1, des méthodes d’aide dans ResponseEntity pourraient être utilisées comme

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

et

return ResponseEntity.ok(json);
569
Bassem Reda Zohdy

Quelque chose comme ça devrait marcher, je ne sais pas s'il existe 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;
}
97
stacker

Pas nécessairement la manière la plus compacte de le faire, mais tout à fait propre OMI

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 ModelAndView ou quelque chose d'autre.

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

51
Zutty

Je changerais légèrement l'implémentation:

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 [ResponseStatusExceptionResolver]. Si l'exception est levée, une réponse sera créée avec le statut de réponse correspondant. (J'ai également pris la liberté de changer le code d'état en 404 - Not Found, ce que je trouve plus approprié pour ce cas d'utilisation, mais vous pouvez vous en tenir à HttpStatus.BAD_REQUEST si vous le souhaitez.)


Ensuite, je changerais le MatchService pour avoir la signature suivante:

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

Enfin, je mettrais à jour le contrôleur et le délégué au MappingJackson2HttpMessageConverter de Spring pour gérer la sérialisation JSON automatiquement (il est ajouté par défaut si vous ajoutez Jackson au chemin de classe et ajoutez soit @EnableWebMvc ou <mvc:annotation-driven /> à votre configuration, voir le référence docs ):

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

Notez qu'il est très courant de séparer les objets de domaine des objets de vue ou des objets DTO. Cela peut être facilement 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);
}
46
matsev

Voici une approche différente. Créez un Exception personnalisé annoté avec @ResponseStatus, comme le suivant.

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

    public NotFoundException() {
    }
}

Et jetez-le quand vous en aurez 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 de Spring ici: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .

31
danidemi

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 je suis venu à la place.

  • Créer une exception générique qui accepte un statut HTTP
  • Créer un gestionnaire d'exception d'avis de contrôleur

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

Puis je crée une classe de conseil 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/

17
Norris

Je l’utilise dans mon application de démarrage de printemps

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

Avec Spring Boot, je ne suis pas tout à fait sûr pourquoi cela était nécessaire (j'ai le repli /error même si @ResponseBody a été défini sur un @ExceptionHandler), mais ce qui suit ne fonctionne pas :

@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 lancé une exception, apparemment parce qu'aucun type de média pouvant être produit n'a été défini comme un attribut de requête:

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

Alors je les ai 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 aussi ajouté @JsonProperty annotation

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

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

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

Vous pouvez également simplement throw new HttpMessageNotReadableException("error description") bénéficier du traitement de Spring traitement des erreurs par défaut .

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

Je trouve ces informations utiles lors du rejet de demandes qui auraient raisonnablement pu être fabriquées à 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

0
dtk