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


389

Estoy usando Spring MVC para una API JSON simple, con un @ResponseBodyenfoque basado 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 ?

Encontré 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.

Respuestas:


624

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

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

y por solicitud correcta

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

ACTUALIZACIÓN 1

después de la primavera 4.1 hay métodos auxiliares en ResponseEntity que podrían usarse como

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

y

return ResponseEntity.ok(json);

Ah, entonces también puedes usarlo ResponseEntityasí. Esto funciona bien y es solo un simple cambio al código original, ¡gracias!
Jonik

eres bienvenido en cualquier momento que puedas agregar un encabezado personalizado también. Comprueba todos los constructores de ResponseEntity
Bassem Reda Zohdy

77
¿Qué pasa si está pasando algo más que una cuerda? Como en un POJO u otro objeto?
mrshickadance

11
será 'ResponseEntity <YourClass>'
Bassem Reda Zohdy

55
Con este enfoque ya no necesita la anotación
@ResponseBody

108

Algo como esto debería funcionar, no estoy seguro de si hay una forma más simple o no:

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

55
¡Gracias! Esto funciona y es bastante simple también. (En este caso se podría simplificarse aún más mediante la eliminación de la no utilizado bodyy requestparams.)
Jonik

54

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

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

Editar puede usar @ResponseBody en el método del controlador de excepciones si usa Spring 3.1+, de lo contrario use ao ModelAndViewalgo.

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


1
Lo siento, esto no parece funcionar. Produce un "error de servidor" HTTP 500 con un seguimiento largo de la pila en los registros: 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 representation¿falta algo en la respuesta?
Jonik

Además, no entendí completamente el punto de definir otro tipo personalizado (MyError). ¿Es eso necesario? Estoy usando la última Spring (3.2.2).
Jonik

1
Esto funciona para mi. Yo uso javax.validation.ValidationExceptionen su lugar. (Primavera 3.1.4)
Jerry Chen el

Esto es bastante útil en situaciones donde tiene una capa intermedia entre su servicio y el cliente donde la capa intermedia tiene sus propias capacidades de manejo de errores. Gracias por este ejemplo @Zutty
StormeHawke

Esta debería ser la respuesta aceptada, ya que mueve el código de manejo de excepciones fuera del flujo normal y oculta HttpServlet *
lilalinux

48

Cambiaría la implementación ligeramente:

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's 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 al 404 - Not Foundque me parece más apropiado para este caso de uso, pero puede seguirlo HttpStatus.BAD_REQUESTsi lo desea).


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

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

Finalmente, actualizaría el controlador y delegaría en Spring's MappingJackson2HttpMessageConverterpara manejar la serialización JSON automáticamente (se agrega de forma predeterminada si agrega Jackson al classpath y agrega @EnableWebMvco <mvc:annotation-driven />a su configuración, vea 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 de vista u objetos DTO. Esto se puede lograr fácilmente agregando una pequeña fábrica de DTO 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);
}

Tengo 500 e inicio de sesión: 28 de agosto de 2015, 5:23:31 p.m. org.apache.cxf.interceptor.Fault
maquinilla de afeitar

Solución perfecta, solo quiero agregar que espero que el DTO sea una composición de Matchalgún otro objeto.
Marco Sulla

32

Aquí hay un enfoque diferente. Cree una Exceptionanotación personalizada con @ResponseStatus, como la siguiente.

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

    public NotFoundException() {
    }
}

Y tíralo 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;
}

Consulte la documentación de Spring aquí: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .


Este enfoque le permite terminar la ejecución donde sea que se encuentre en el stacktrace sin tener que devolver un "valor especial" que debería especificar el código de estado HTTP que desea devolver.
Muhammad Gelbana

21

Como se menciona en algunas respuestas, existe la posibilidad 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.

  • Cree una excepción genérica que acepte un estado HTTP
  • Crear un controlador de excepciones de consejos de controlador

Vayamos 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 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 usarlo

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

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


Método muy bueno .. En lugar de una simple cadena prefiero devolver un JSON con campos errorCode y mensajes ..
Ismail Yavuz

1
Esta debería ser la respuesta correcta, un controlador de excepciones genérico y global con un código de estado personalizado y un mensaje: D
Pedro Silva

10

Estoy usando esto en mi aplicación de arranque de 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);
}

9

La forma más fácil es lanzar 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;
    }

3
La mejor respuesta: no es necesario cambiar el tipo de retorno y no es necesario crear su propia excepción. Además, ResponseStatusException permite agregar un mensaje de razón si es necesario.
Migra

Es importante tener en cuenta que ResponseStatusException solo está disponible en Spring versión 5+
Ethan Conner

2

Con Spring Boot, no estoy completamente seguro de por qué esto era necesario (obtuve el /errorrespaldo aunque @ResponseBodyse 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 arrojó 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);

Entonces 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 compatible", pero aún así no funcionó, porque ErrorMessageestaba defectuoso:

public class ErrorMessage {
    int code;

    String message;
}

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

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

También podría throw new HttpMessageNotReadableException("error description")beneficiarse del manejo de errores predeterminado de Spring .

Sin embargo, tal como es el caso con esos errores predeterminados, no se establecerá ningún cuerpo de respuesta.

Encuentro esto útil al rechazar solicitudes que razonablemente solo podrían haber sido hechas a mano, lo que potencialmente indica una intención malévola, ya que ocultan el hecho de que la solicitud fue rechazada en base a una validación personalizada más profunda y sus criterios.

Hth, dtk


HttpMessageNotReadableException("error description")es obsoleto.
Kuba Šimonovský

0

Otro enfoque es utilizar @ExceptionHandlerla @ControllerAdvicede centralizar todas sus manipuladores en la misma clase, si no hay que poner los métodos de controlador en cada controlador que desea gestionar una excepción.

Tu clase de manejador:

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

Tu excepción personalizada:

public class MyBadRequestException extends RuntimeException {

  private String description;

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

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

Ahora puede lanzar excepciones desde cualquiera de sus controladores, y puede definir otros controladores dentro de su clase de consejos.


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.