java - custom - ¿Cómo responder con el error HTTP 400 en un método Spring MVC @ResponseBody que devuelve String?
spring boot return http status (9)
Estoy usando Spring MVC para una API JSON simple, con @ResponseBody
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í porque el tipo de retorno de mi método es String, no ResponseEntity.
Algo como esto debería funcionar, no estoy seguro de si hay o no una forma 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;
}
Aquí hay un enfoque diferente. Cree una Exception
personalizada anotada con @ResponseStatus
, como la siguiente.
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Not Found")
public class NotFoundException extends Exception {
public NotFoundException() {
}
}
Y tirarlo 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 .
Cambiaría ligeramente la implementación:
Primero, creo una 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 el ResponseStatusExceptionResolver
de Spring. Si se lanza la excepción, se 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
para tener la siguiente firma:
interface MatchService {
public Match findMatch(String matchId);
}
Finalmente, actualizaría el controlador y delegaría a MappingJackson2HttpMessageConverter
de Spring para manejar la serialización JSON automáticamente (se agrega de manera predeterminada si agrega Jackson a la ruta de @EnableWebMvc
y agrega @EnableWebMvc
o <mvc:annotation-driven />
@EnableWebMvc
<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 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);
}
Como se mencionó en algunas respuestas, existe la posibilidad de crear una clase de excepción para cada estado HTTP que desea devolver. No me gusta la idea de tener que crear una clase por estado para cada proyecto. Aquí está 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
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;
}
}
Entonces creo una clase de consejos 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/
Con Spring Boot, no estoy completamente seguro de por qué fue necesario (obtuve el /error
@ResponseBody
pesar de que @ResponseBody
se definió en un @ExceptionHandler
), pero lo siguiente en sí 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 que se pudieran producir 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 compatible", pero aún así no funcionó, porque mi ErrorMessage
era defectuoso:
public class ErrorMessage {
int code;
String message;
}
JacksonMapper no lo manejó como "convertible", así que tuve que agregar getters / setters, y también @JsonProperty
anotación @JsonProperty
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."}
Creo que este hilo en realidad tiene la solución más fácil y limpia, que no sacrifica las herramientas de marcaje JSON que proporciona Spring:
https://.com/a/16986372/1278921
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);
}
No necesariamente la forma más compacta de hacer esto, pero IMO 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");
}
Edite puede usar @ResponseBody en el método del controlador de excepciones si usa Spring 3.1+, de lo contrario use un ModelAndView
o algo así.
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 correcta solicitud
return new ResponseEntity<>(json,HttpStatus.OK);
ACTUALIZACIÓN 1
después de la primavera 4.1 hay métodos de ayuda en ResponseEntity que podrían usarse como
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
y
return ResponseEntity.ok(json);