How to respond with an HTTP 400 error in a Spring MVC @ResponseBody method returning String
Asked Answered
H

13

457

I'm using Spring MVC for a simple JSON API, with a @ResponseBody based approach like the following. (I already have a service layer producing JSON directly.)

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

In the given scenario, what is the simplest and cleanest way to give a response with an HTTP 400 error?

I did come across approaches like:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

...but I can't use it here since my method's return type is String, not ResponseEntity.

Humanly answered 26/4, 2013 at 9:16 Comment(0)
E
716

Change your return type to ResponseEntity<>, and then you can use the below for 400:

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

And for a correct request:

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

After Spring 4.1 there are helper methods in ResponseEntity which could be used as:

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

and

return ResponseEntity.ok(json);
Erfurt answered 27/4, 2013 at 10:1 Comment(11)
Ah, so you can use ResponseEntity like this too. This works nicely and is a just a simple change to the original code—thanks!Humanly
you are welcome any time you can add custom header too check all constructors of ResponseEntityErfurt
you may optionally provide a response body even for response codes besides HttpStatus.OK. it works.Sickly
What if you are passing something other than a string back? As in a POJO or other object?Pusher
it will be 'ResponseEntity<YourClass>'Erfurt
Using this approach you don't need @ResponseBody annotation any moreKathrynkathryne
Even with this, I still get a 200 response status - Server responded with a response on thread XNIO-3 task-2 2 < 200 2 < Content-Type: application/json although the response body contains a statusCodeValue which is set to 400 - { "headers": {}, "body": null, "statusCode": "BAD_REQUEST", "statusCodeValue": 400 }Centripetal
@SandeepanNath this answer is 5 years back and you can see that it is accepted by big enough number to prove it is working with them, may be you are not implemented well or framework version you used have an issue, please share your code on github and let me check it for you.Erfurt
But then you lose the type the Response normally returns, which won't work well with Swagger generated documentation.Jordans
ResponseEntity<?> you can add response type to your ResponseEntity.Erfurt
It is also possible to use return ResponseEntity.badRequest(), also with body return ResponseEntity.badRequest().body(json); Gregoriogregorius
P
119

Something like this should work, but I'm not sure whether or not there is a simpler way:

@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;
}
Professoriate answered 26/4, 2013 at 9:35 Comment(2)
Thanks! This works and is pretty simple too. (In this case it could be further simplified by removing the unused body and request params.)Humanly
What I'm not clear about is how to set the Http status for a successful return, rather than an error.Maurinemaurise
T
57

It is not necessarily the most compact way of doing this, but quite clean in my opinion:

if(json == null) {
    throw new BadThingException();
}
...

@ExceptionHandler(BadThingException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public @ResponseBody MyError handleException(BadThingException e) {
    return new MyError("That doesn’t work");
}

You can use @ResponseBody in the exception handler method if using Spring 3.1+, otherwise use a ModelAndView or something.

@ResponseBody does not work with @ExceptionHandler [SPR-6902] #11567

Thibodeau answered 26/4, 2013 at 9:19 Comment(8)
Sorry, this doesn't seem to work. It produces HTTP 500 "server error" with long stack trace in logs: 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 Is there something missing from the answer?Humanly
Also, I didn't fully understand the point of defining yet another custom type (MyError). Is that necessary? I'm using latest Spring (3.2.2).Humanly
It works for me. I use javax.validation.ValidationException instead. (Spring 3.1.4)Hatching
This is quite useful in situations where you have an intermediate layer between your service and the client where the intermediate layer has its own error handling capabilities. Thank you for this example @ThibodeauYehudit
This should be the accepted answer, as it moves the exception handling code out of the normal flow and it hides HttpServlet*Delladelle
It isn't working for me with Spring Boot and I really don't know whyReek
This answer is nice because is allows the normal return type and the error-case return type be completely different. (I have a function that normally returns a Dog object that gets JSON serialized by Spring Boot, but when the request is bad, I return a completely different Error-type message, unrelated to Dog.)Singular
@Humanly FWIW I encountered Could not find acceptable representation because Jackson couldn't serialize the object (which could be for many reasons). In any case, a custom type isn't necessary either. Check out https://mcmap.net/q/80251/-how-to-respond-with-an-http-400-error-in-a-spring-mvc-responsebody-method-returning-string which I think is the cleanest and doesn't require changing your return type i.e. controller throws exceptionLyle
F
51

I would change the implementation slightly:

First, I create a UnknownMatchException:

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

Note the use of @ResponseStatus, which will be recognized by Spring's ResponseStatusExceptionResolver. If the exception is thrown, it will create a response with the corresponding response status. (I also took the liberty of changing the status code to 404 - Not Found which I find more appropriate for this use case, but you can stick to HttpStatus.BAD_REQUEST if you like.)


Next, I would change the MatchService to have the following signature:

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

Finally, I would update the controller and delegate to Spring's MappingJackson2HttpMessageConverter to handle the JSON serialization automatically (it is added by default if you add Jackson to the classpath and add either @EnableWebMvc or <mvc:annotation-driven /> to your config. See the reference documentation):

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

Note, it is very common to separate the domain objects from the view objects or DTO objects. This can easily be achieved by adding a small DTO factory that returns the serializable JSON object:

@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);
}
Fettle answered 27/4, 2013 at 10:10 Comment(3)
I have 500 and i logs: ay 28, 2015 5:23:31 PM org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERE: Error occurred during error handling, give up! org.apache.cxf.interceptor.FaultAlec
Perfect solution, I want only to add that I hope that the DTO is a composition of Match and some other object.Menis
The problem with this approach is that the JavaDocs for the @ResponseStatus annotation tell us not to do this for a RESTful server. Using this annotation causes the server to send an HTML response, which is fine for a web application but not for a Restful service. However, a new RuntimeException has been defined called ResponseStatusException which probably serves the same purpose, although I don't have much experience with it yet, but it doesn't come with the same warning as @ResponseStatus.Maurinemaurise
P
41

Here's a different approach. Create a custom Exception annotated with @ResponseStatus, like the following one.

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

    public NotFoundException() {
    }
}

And throw it when needed.

@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;
}
Pigweed answered 5/8, 2016 at 16:20 Comment(4)
This approach allows you to terminate execution wherever you are in the stacktrace without having to return a "special value" that should specify the HTTP status code you wish to return.Jonijonie
The link is (effectively) broken. There isn't anything about exceptions on it.Tildy
Hi @PeterMortensen I was unable to found a replacement for the broken link so I removed it.Pigweed
The problem with this approach is that the JavaDocs for the @ResponseStatus annotation tell us not to do this for a RESTful server. Using this annotation causes the server to send an HTML response, which is fine for a web application but not for a Restful service. However, a new RuntimeException has been defined called ResponseStatusException which probably serves the same purpose, although I don't have much experience with it yet, but it doesn't come with the same warning as @ResponseStatus.Maurinemaurise
M
38

The easiest way is to throw 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;
}
Misdemeanant answered 15/1, 2020 at 12:58 Comment(6)
Best answer: no need to change the return type and no need to create your own exception. Also, the ResponseStatusException allows to add a reason message if needed.Schmitz
Important to note that ResponseStatusException is only available in Spring version 5+Esculent
this answer should be on the topRid
You can't have a JSON body as the response.Trappings
This should be mark as answer, useful on web controller and no need change the return type.Erythritol
What's not clear to me is this: What determines the status code when there's no error? And how would I set that? And what does it default to?Maurinemaurise
L
26

As mentioned in some answers, there is the ability to create an exception class for each HTTP status that you want to return. I don't like the idea of having to create a class per status for each project. Here is what I came up with instead.

  • Create a generic exception that accepts an HTTP status
  • Create an Controller Advice exception handler

Let's get to the 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;
    }
}

Then I create a controller advice class

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

To use it

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

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

Lientery answered 10/11, 2016 at 0:38 Comment(3)
Very good method.. Instead of a simple String I prefer to return a jSON with errorCode and message fields..Devote
This should be the correct answer, a generic and global exception handler with custom status code and message :DConvertite
Browsers don't link the link: "Warning: Potential Security Risk Ahead ... The certificate for javaninja.net expired on 11/18/2021."Tildy
O
13

I’m using this in my Spring Boot application:

@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);
}
Outlive answered 13/10, 2015 at 12:45 Comment(1)
An explanation would be in order. E.g., what is the idea/gist? From the Help Center: "...always explain why the solution you're presenting is appropriate and how it works". Please respond by editing (changing) your answer, not here in comments (without "Edit:", "Update:", or similar - the answer should appear as if it was written today).Tildy
R
3

With Spring Boot, I'm not entirely sure why this was necessary (I got the /error fallback even though @ResponseBody was defined on an @ExceptionHandler), but the following in itself did not work:

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

It still threw an exception, apparently because no producible media types were defined as a request attribute:

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

So I added them.

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

And this got me through to have a "supported compatible media type", but then it still didn't work, because my ErrorMessage was faulty:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper did not handle it as "convertable", so I had to add getters/setters, and I also added @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;
    }
}

Then I received my message as intended

{"code":400,"message":"An \"url\" parameter must be defined."}
Reek answered 1/12, 2016 at 15:41 Comment(0)
G
2

Another approach is to use @ExceptionHandler with @ControllerAdvice to centralize all your handlers in the same class. If not, you must put the handler methods in every controller you want to manage an exception for.

Your handler class:

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

Your custom exception:

public class MyBadRequestException extends RuntimeException {

  private String description;

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

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

Now you can throw exceptions from any of your controllers, and you can define other handlers inside you advice class.

Gilthead answered 16/1, 2020 at 15:21 Comment(0)
L
1

The simplest and cleanest way to handle exceptions in your controller without having to explicitly return ResponseEntity is to just add @ExceptionHandler methods.

Example snippet using Spring Boot 2.0.3.RELEASE:

// Prefer static import of HttpStatus constants as it's cleaner IMHO

// Handle with no content returned
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(BAD_REQUEST)
void onIllegalArgumentException() {}

// Return 404 when JdbcTemplate does not return a single row
@ExceptionHandler(IncorrectResultSizeDataAccessException.class)
@ResponseStatus(NOT_FOUND)
void onIncorrectResultSizeDataAccessException() {}

// Catch all handler with the exception as content
@ExceptionHandler(Exception.class)
@ResponseStatus(I_AM_A_TEAPOT)
@ResponseBody Exception onException(Exception e) {
  return e;
}

As an aside:

  • If in all contexts/usages, matchService.getMatchJson(matchId) == null is invalid, then my suggestion would be to have getMatchJson throw an exception, e.g., IllegalArgumentException instead of returning null and let it bubble up to the controller's @ExceptionHandler.

  • If null is used to test other conditions then I would have a specific method, e.g., matchService.hasMatchJson(matchId). In general, I avoid null if possible in order to avoid an unexpected NullPointerException.

Lyle answered 9/12, 2021 at 0:8 Comment(0)
M
0

You also could just throw new HttpMessageNotReadableException("error description") to benefit from Spring's default error handling.

However, just as is the case with those default errors, no response body will be set.

I find these useful when rejecting requests that could reasonably only have been handcrafted, potentially indicating a malevolent intent, since they obscure the fact that the request was rejected based on a deeper, custom validation and its criteria.

Mala answered 9/7, 2019 at 21:57 Comment(1)
HttpMessageNotReadableException("error description") is deprecated.Osmen
W
-1

Use a custom response with the status code.

Like this:

class Response<T>(
    val timestamp: String = DateTimeFormatter
            .ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
            .withZone(ZoneOffset.UTC)
            .format(Instant.now()),
    val code: Int = ResultCode.SUCCESS.code,
    val message: String? = ResultCode.SUCCESS.message,
    val status: HttpStatus = HttpStatus.OK,
    val error: String? = "",
    val token: String? = null,
    val data: T? = null
) : : ResponseEntity<Response.CustomResponseBody>(status) {

data class CustomResponseBody(
    val timestamp: String = DateTimeFormatter
            .ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
            .withZone(ZoneOffset.UTC)
            .format(Instant.now()),
    val code: Int = ResultCode.SUCCESS.code,
    val message: String? = ResultCode.SUCCESS.message,
    val error: String? = "",
    val token: String? = null,
    val data: Any? = null
)

override fun getBody(): CustomResponseBody? = CustomResponseBody(timestamp, code, message, error, token, data)
Wonted answered 28/6, 2021 at 8:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.