¿Cómo responder con un error HTTP 400 en un método MVC @ResponseBody de Spring que devuelve una cadena?


Estoy usando Spring MVC para una API JSON simple, con un enfoque basado en @ResponseBody como el siguiente. (Ya tengo una capa de servicio que produce JSON directamente.)

@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 pregunta es, en el escenario dado, ¿cuál es la forma más simple y limpia de responder con un error HTTP 400?

Me encontré con enfoques como:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

...pero no puedo usarlo aquí ya que el tipo de retorno de mi método es String, no ResponseEntity.

Author: abhi, 2013-04-26

9 answers

Cambie su tipo de devolución a ResponseEntity<>, luego puede usar a continuación para 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

Y para la solicitud correcta

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

ACTUALIZACIÓN 1

Después de la primavera 4.1 hay métodos de ayuda en ResponseEntity podría ser utilizado como

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

Y

return ResponseEntity.ok(json);
 488
Author: Bassem Reda Zohdy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-06 11:16:19

Algo como esto debería funcionar, no estoy seguro de si hay o no una manera más 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;
}
 86
Author: stacker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-27 09:14:43

No necesariamente la forma más compacta de hacer esto, pero bastante limpio IMO

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

Edit puedes usar @ResponseBody en el método exception handler si usas Spring 3.1+, de lo contrario usa un ModelAndView o algo así.

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

 47
Author: Zutty,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-26 09:25:59

Cambiaría ligeramente la implementación:

Primero, creo un UnknownMatchException:

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

Tenga en cuenta el uso de @ResponseStatus, que será reconocido por Spring ResponseStatusExceptionResolver. Si se lanza la excepción, creará una respuesta con el estado de respuesta correspondiente. (También me tomé la libertad de cambiar el código de estado a 404 - Not Found que me parece más apropiado para este caso de uso, pero puede atenerse a HttpStatus.BAD_REQUEST si lo desea.)


A continuación, cambiaría el MatchService la siguiente firma:

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

Finalmente, actualizaría el controlador y delegaría a Spring MappingJackson2HttpMessageConverter para manejar la serialización JSON automáticamente (se agrega por defecto si agrega Jackson al classpath y agrega @EnableWebMvc o <mvc:annotation-driven /> a su configuración, consulte los documentos de referencia ):

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

Tenga en cuenta que es muy común separar los objetos de dominio de los objetos view u objetos DTO. Esto se puede lograr fácilmente mediante la adición de un pequeño DTO fábrica que devuelve el objeto JSON serializable:

@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);
}
 41
Author: matsev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-11 21:18:49

Aquí hay un enfoque diferente. Cree un Exception personalizado anotado con @ResponseStatus, como el siguiente.

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

    public NotFoundException() {
    }
}

Y lanzarlo cuando sea necesario.

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

Echa un vistazo a la documentación de primavera aquí: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions.

 29
Author: danidemi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-05 16:20:39

Como se mencionó en algunas respuestas, existe la capacidad de crear una clase de excepción para cada estado HTTP que desee devolver. No me gusta la idea de tener que crear una clase por estado para cada proyecto. Esto es lo que se me ocurrió en su lugar.

  • Crear una excepción genérica que acepte un estado HTTP
  • Crear un Controlador de excepción de Asesoramiento

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

Luego creo una clase de consejo del 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());
    }
}

A usar

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

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

 12
Author: Norris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-11-10 00:38:06

Estoy usando esto en mi aplicación 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);
}
 8
Author: Aamir Faried,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-24 18:10:16

Con Spring Boot, no estoy del todo seguro de por qué esto era necesario (obtuve el /error respaldo a pesar de que @ResponseBody se definió en un @ExceptionHandler), pero lo siguiente en sí mismo no funcionó:

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

Todavía lanzó una excepción, aparentemente porque no se definieron tipos de medios producibles como un atributo de solicitud:

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

Así que los agregué.

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

Y esto me ayudó a tener un "tipo de medio compatible soportado", pero aún así no funcionó, porque mi ErrorMessage era defectuoso:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper no lo manejaba como "convertible", así que tuve que agregar getters/setters, y también agregué @JsonProperty anotación

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

Entonces recibí mi mensaje como estaba previsto

{"code":400,"message":"An \"url\" parameter must be defined."}
 0
Author: EpicPandaForce,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-01 15:41:05

Creo que este hilo realmente tiene la solución más fácil y limpia, que no sacrifica las herramientas de martialing JSON que Spring proporciona:

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

 -2
Author: Ryan S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:18:25