Come rispondere con errore HTTP 400 in un metodo Spring MVC @ResponseBody che restituisce String?


389

Sto usando Spring MVC per una semplice API JSON, con un @ResponseBodyapproccio basato come il seguente. (Ho già un livello di servizio che produce direttamente 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 domanda è, in un determinato scenario, qual è il modo più semplice e pulito per rispondere con un errore HTTP 400 ?

Mi sono imbattuto in approcci come:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

... ma non posso usarlo qui poiché il tipo di ritorno del mio metodo è String, non ResponseEntity.

Risposte:


624

cambiare il tipo di ritorno in ResponseEntity<>, quindi è possibile utilizzare di seguito per 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

e per una corretta richiesta

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

AGGIORNAMENTO 1

dopo la primavera 4.1 ci sono metodi di supporto in ResponseEntity che possono essere usati come

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

e

return ResponseEntity.ok(json);

Ah, quindi puoi usare ResponseEntityanche questo. Funziona bene ed è solo una semplice modifica al codice originale — grazie!
Jonik,

sei il benvenuto ogni volta che puoi aggiungere anche un'intestazione personalizzata controlla tutti i costruttori di ResponseEntity
Bassem Reda Zohdy

7
Che cosa succede se stai passando qualcosa di diverso da una stringa indietro? Come in un POJO o in un altro oggetto?
Mrshickadance,

11
sarà "ResponseEntity <YourClass>"
Bassem Reda Zohdy,

5
Usando questo approccio non hai più bisogno dell'annotazione
@ResponseBody

108

Qualcosa del genere dovrebbe funzionare, non sono sicuro che esista o meno un modo più semplice:

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

5
Grazie! Funziona ed è anche abbastanza semplice. (In questo caso potrebbe essere ulteriormente semplificato rimuovendo gli inutilizzati bodye i requestparametri.)
Jonik,

54

Non necessariamente il modo più compatto per farlo, ma IMO abbastanza pulito

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

Modifica puoi usare @ResponseBody nel metodo del gestore delle eccezioni se usi Spring 3.1+, altrimenti usa a ModelAndViewo qualcosa del genere.

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


1
Mi dispiace, questo non sembra funzionare. Produce "errore del server" HTTP 500 con traccia di stack lunghi nei registri: 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 representationmanca qualcosa nella risposta?
Jonik,

Inoltre, non ho compreso appieno il punto di definire ancora un altro tipo personalizzato (MyError). È necessario? Sto usando l'ultima primavera (3.2.2).
Jonik,

1
Per me funziona. Io uso javax.validation.ValidationExceptioninvece. (Primavera 3.1.4)
Jerry Chen,

Ciò è abbastanza utile in situazioni in cui esiste un livello intermedio tra il servizio e il client in cui il livello intermedio ha le proprie capacità di gestione degli errori. Grazie per questo esempio @Zutty
StormeHawke,

Questa dovrebbe essere la risposta accettata, poiché sposta il codice di gestione delle eccezioni fuori dal normale flusso e nasconde HttpServlet *
lilalinux

48

Vorrei modificare leggermente l'implementazione:

Innanzitutto, creo un UnknownMatchException:

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

Nota l'uso di @ResponseStatus , che sarà riconosciuto da Spring ResponseStatusExceptionResolver. Se viene generata l'eccezione, verrà creata una risposta con lo stato di risposta corrispondente. (Mi sono anche preso la libertà di modificare il codice di stato a 404 - Not Foundcui trovo più appropriato per questo caso d'uso, ma puoi attenerti HttpStatus.BAD_REQUESTse vuoi.)


Successivamente, cambierei il MatchServiceper avere la seguente firma:

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

Infine, aggiornerei il controller e delegare a Spring's MappingJackson2HttpMessageConverterper gestire automaticamente la serializzazione JSON (viene aggiunto per impostazione predefinita se aggiungi Jackson al percorso di classe e aggiungi uno @EnableWebMvco <mvc:annotation-driven />alla tua configurazione, vedi i documenti di riferimento ):

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

Nota, è molto comune separare gli oggetti dominio dagli oggetti vista o DTO. Ciò può essere facilmente ottenuto aggiungendo un piccolo factory DTO che restituisce l'oggetto JSON serializzabile:

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

Ho 500 e registri: ay 28, 2015 17:23:31 org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERE: errore durante la gestione degli errori, rinunciare! org.apache.cxf.interceptor.Fault
razor

Soluzione perfetta, voglio solo aggiungere che spero che il DTO sia una composizione Matche qualche altro oggetto.
Marco Sulla

32

Ecco un approccio diverso. Crea Exceptionun'annotazione personalizzata con @ResponseStatus, come la seguente.

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

    public NotFoundException() {
    }
}

E lanciarlo quando necessario.

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

Consulta la documentazione di primavera qui: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .


Questo approccio consente di terminare l'esecuzione ovunque ci si trovi nello stacktrace senza dover restituire un "valore speciale" che dovrebbe specificare il codice di stato HTTP che si desidera restituire.
Muhammad Gelbana,

21

Come indicato in alcune risposte, esiste la possibilità di creare una classe di eccezione per ogni stato HTTP che si desidera restituire. Non mi piace l'idea di dover creare una classe per stato per ogni progetto. Ecco cosa invece mi è venuta in mente.

  • Creare un'eccezione generica che accetta uno stato HTTP
  • Creare un gestore eccezioni Controller Advice

Andiamo al codice

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

Quindi creo una classe di consulenza sul controller

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

Per usarlo

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

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


Metodo molto buono .. Invece di una semplice stringa, preferisco restituire un jSON con campi errorCode e message ..
İsmail Yavuz

1
Questa dovrebbe essere la risposta corretta, un gestore di eccezioni generico e globale con codice di stato personalizzato e messaggio: D
Pedro Silva,

10

Sto usando questo nella mia applicazione di avvio a molla

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

9

Il modo più semplice è lanciare a 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;
    }

3
Migliore risposta: non è necessario modificare il tipo di ritorno e non è necessario creare la propria eccezione. Inoltre, ResponseStatusException consente di aggiungere un messaggio di motivazione, se necessario.
Migs

È importante notare che ResponseStatusException è disponibile solo nella versione Spring 5+
Ethan Conner

2

Con Spring Boot, non sono del tutto sicuro del motivo per cui era necessario (ho ottenuto il /errorfallback anche se è @ResponseBodystato definito su un @ExceptionHandler), ma il seguente in sé non ha funzionato:

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

Ha comunque generato un'eccezione, apparentemente perché nessun tipo di supporto producibile è stato definito come un attributo di richiesta:

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

Quindi li ho aggiunti.

@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 questo mi ha permesso di avere un "tipo di supporto compatibile supportato", ma poi non ha funzionato, perché il mio ErrorMessageera difettoso:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper non lo gestiva come "convertibile", quindi ho dovuto aggiungere getter / setter e ho anche aggiunto @JsonPropertyun'annotazione

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

Quindi ho ricevuto il mio messaggio come previsto

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

0

Potresti anche throw new HttpMessageNotReadableException("error description")trarre vantaggio dalla gestione degli errori predefinita di Spring .

Tuttavia, proprio come nel caso di tali errori predefiniti, non verrà impostato alcun corpo di risposta.

Trovo che siano utili quando si respingono richieste che potevano ragionevolmente essere state solo realizzate a mano, indicando potenzialmente un intento malevolo, poiché oscurano il fatto che la richiesta è stata respinta in base a una convalida personalizzata più profonda e ai suoi criteri.

Ht, dtk


HttpMessageNotReadableException("error description")è deprecato.
Kuba Šimonovský,

0

Un altro approccio è usare @ExceptionHandlercon@ControllerAdvice per centralizzare tutti i gestori nella stessa classe, se non è necessario mettere i metodi dei gestori in ogni controller che si desidera gestire un'eccezione.

La tua classe gestore:

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

La tua eccezione personalizzata:

public class MyBadRequestException extends RuntimeException {

  private String description;

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

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

Ora puoi generare eccezioni da qualsiasi controller e puoi definire altri gestori all'interno della tua classe di consulenza.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.