I am trying to set up a large-scale REST services server. We're using Spring Boot 1.2.1 Spring 4.1.5, and Java 8. Our controllers are implementing @RestController and the standard @RequestMapping annotations.
My problem is that Spring Boot sets up a default redirect for controller exceptions to /error
. From the docs:
Spring Boot provides an /error mapping by default that handles all errors in a sensible way, and it is registered as a ‘global’ error page in the servlet container.
Coming from years writing REST applications with Node.js, this is, to me, anything but sensible. Any exception a service endpoint generates should return in the response. I can't understand why you'd send a redirect to what is most likely an Angular or JQuery SPA consumer which is only looking for an answer and can't or won't take any action on a redirect.
What I want to do is set up a global error handler that can take any exception - either purposefully thrown from a request mapping method or auto-generated by Spring (404 if no handler method is found for the request path signature), and return a standard formatted error response (400, 500, 503, 404) to the client without any MVC redirects. Specifically, we are going to take the error, log it to NoSQL with a UUID, then return to the client the right HTTP error code with the UUID of the log entry in the JSON body.
The docs have been vague on how to do this. It seems to me that you have to either create your own ErrorController implementation or use ControllerAdvice in some fashion, but all the examples I've seen still include forwarding the response to some kind of error mapping, which doesn't help. Other examples suggest that you'd have to list every Exception type you want to handle instead of just listing "Throwable" and getting everything.
Can anyone tell me what I missed, or point me in the right direction on how to do this without suggesting up the chain that Node.js would be easier to deal with?
This question is related to
java
spring
rest
exception-handling
spring-boot
@RestControllerAdvice is a new feature of Spring Framework 4.3 to handle Exception with RestfulApi by a cross-cutting concern solution:
package com.khan.vaquar.exception;
import javax.servlet.http.HttpServletRequest;
import org.owasp.esapi.errors.IntrusionException;
import org.owasp.esapi.errors.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.khan.vaquar.domain.ErrorResponse;
/**
* Handles exceptions raised through requests to spring controllers.
**/
@RestControllerAdvice
public class RestExceptionHandler {
private static final String TOKEN_ID = "tokenId";
private static final Logger log = LoggerFactory.getLogger(RestExceptionHandler.class);
/**
* Handles InstructionExceptions from the rest controller.
*
* @param e IntrusionException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IntrusionException.class)
public ErrorResponse handleIntrusionException(HttpServletRequest request, IntrusionException e) {
log.warn(e.getLogMessage(), e);
return this.handleValidationException(request, new ValidationException(e.getUserMessage(), e.getLogMessage()));
}
/**
* Handles ValidationExceptions from the rest controller.
*
* @param e ValidationException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = ValidationException.class)
public ErrorResponse handleValidationException(HttpServletRequest request, ValidationException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
if (e.getUserMessage().contains("Token ID")) {
tokenId = "<OMITTED>";
}
return new ErrorResponse( tokenId,
HttpStatus.BAD_REQUEST.value(),
e.getClass().getSimpleName(),
e.getUserMessage());
}
/**
* Handles JsonProcessingExceptions from the rest controller.
*
* @param e JsonProcessingException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = JsonProcessingException.class)
public ErrorResponse handleJsonProcessingException(HttpServletRequest request, JsonProcessingException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.BAD_REQUEST.value(),
e.getClass().getSimpleName(),
e.getOriginalMessage());
}
/**
* Handles IllegalArgumentExceptions from the rest controller.
*
* @param e IllegalArgumentException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public ErrorResponse handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.BAD_REQUEST.value(),
e.getClass().getSimpleName(),
e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = UnsupportedOperationException.class)
public ErrorResponse handleUnsupportedOperationException(HttpServletRequest request, UnsupportedOperationException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.BAD_REQUEST.value(),
e.getClass().getSimpleName(),
e.getMessage());
}
/**
* Handles MissingServletRequestParameterExceptions from the rest controller.
*
* @param e MissingServletRequestParameterException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public ErrorResponse handleMissingServletRequestParameterException( HttpServletRequest request,
MissingServletRequestParameterException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.BAD_REQUEST.value(),
e.getClass().getSimpleName(),
e.getMessage());
}
/**
* Handles NoHandlerFoundExceptions from the rest controller.
*
* @param e NoHandlerFoundException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(value = NoHandlerFoundException.class)
public ErrorResponse handleNoHandlerFoundException(HttpServletRequest request, NoHandlerFoundException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.NOT_FOUND.value(),
e.getClass().getSimpleName(),
"The resource " + e.getRequestURL() + " is unavailable");
}
/**
* Handles all remaining exceptions from the rest controller.
*
* This acts as a catch-all for any exceptions not handled by previous exception handlers.
*
* @param e Exception
* @return error response POJO
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(value = Exception.class)
public ErrorResponse handleException(HttpServletRequest request, Exception e) {
String tokenId = request.getParameter(TOKEN_ID);
log.error(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.INTERNAL_SERVER_ERROR.value(),
e.getClass().getSimpleName(),
"An internal error occurred");
}
}
What about this code ? I use a fallback request mapping to catch 404 errors.
@Controller
@ControllerAdvice
public class ExceptionHandlerController {
@ExceptionHandler(Exception.class)
public ModelAndView exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception ex) {
//If exception has a ResponseStatus annotation then use its response code
ResponseStatus responseStatusAnnotation = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
return buildModelAndViewErrorPage(request, response, ex, responseStatusAnnotation != null ? responseStatusAnnotation.value() : HttpStatus.INTERNAL_SERVER_ERROR);
}
@RequestMapping("*")
public ModelAndView fallbackHandler(HttpServletRequest request, HttpServletResponse response) throws Exception {
return buildModelAndViewErrorPage(request, response, null, HttpStatus.NOT_FOUND);
}
private ModelAndView buildModelAndViewErrorPage(HttpServletRequest request, HttpServletResponse response, Exception ex, HttpStatus httpStatus) {
response.setStatus(httpStatus.value());
ModelAndView mav = new ModelAndView("error.html");
if (ex != null) {
mav.addObject("title", ex);
}
mav.addObject("content", request.getRequestURL());
return mav;
}
}
For people that want to response according to http status code, you can use the ErrorController
way:
@Controller
public class CustomErrorController extends BasicErrorController {
public CustomErrorController(ServerProperties serverProperties) {
super(new DefaultErrorAttributes(), serverProperties.getError());
}
@Override
public ResponseEntity error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status.equals(HttpStatus.INTERNAL_SERVER_ERROR)){
return ResponseEntity.status(status).body(ResponseBean.SERVER_ERROR);
}else if (status.equals(HttpStatus.BAD_REQUEST)){
return ResponseEntity.status(status).body(ResponseBean.BAD_REQUEST);
}
return super.error(request);
}
}
The ResponseBean
here is my custom pojo for response.
Solution with
dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
and
@EnableWebMvc
@ControllerAdvice
worked for me with Spring Boot 1.3.1, while was not working on 1.2.7
By default Spring Boot gives json with error details.
curl -v localhost:8080/greet | json_pp
[...]
< HTTP/1.1 400 Bad Request
[...]
{
"timestamp" : 1413313361387,
"exception" : "org.springframework.web.bind.MissingServletRequestParameterException",
"status" : 400,
"error" : "Bad Request",
"path" : "/greet",
"message" : "Required String parameter 'name' is not present"
}
It also works for all kind of request mapping errors. Check this article http://www.jayway.com/2014/10/19/spring-boot-error-responses/
If you want to create log it to NoSQL. You can create @ControllerAdvice where you would log it and then re-throw the exception. There is example in documentation https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc
With Spring Boot 1.4+ new cool classes for easier exception handling were added that helps in removing the boilerplate code.
A new @RestControllerAdvice
is provided for exception handling, it is combination of @ControllerAdvice
and @ResponseBody
. You can remove the @ResponseBody
on the @ExceptionHandler
method when use this new annotation.
i.e.
@RestControllerAdvice
public class GlobalControllerExceptionHandler {
@ExceptionHandler(value = { Exception.class })
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiErrorResponse unknownException(Exception ex, WebRequest req) {
return new ApiErrorResponse(...);
}
}
For handling 404 errors adding @EnableWebMvc
annotation and the following to application.properties was enough:
spring.mvc.throw-exception-if-no-handler-found=true
You can find and play with the sources here:
https://github.com/magiccrafter/spring-boot-exception-handling
Although this is an older question, I would like to share my thoughts on this. I hope, that it will be helpful to some of you.
I am currently building a REST API which makes use of Spring Boot 1.5.2.RELEASE with Spring Framework 4.3.7.RELEASE. I use the Java Config approach (as opposed to XML configuration). Also, my project uses a global exception handling mechanism using the @RestControllerAdvice
annotation (see later below).
My project has the same requirements as yours: I want my REST API to return a HTTP 404 Not Found
with an accompanying JSON payload in the HTTP response to the API client when it tries to send a request to an URL which does not exist. In my case, the JSON payload looks like this (which clearly differs from the Spring Boot default, btw.):
{
"code": 1000,
"message": "No handler found for your request.",
"timestamp": "2017-11-20T02:40:57.628Z"
}
I finally made it work. Here are the main tasks you need to do in brief:
NoHandlerFoundException
is thrown if API clients
call URLS for which no handler method exists (see Step 1 below).ApiError
) which contains all the data that should be returned to the API client (see step 2).NoHandlerFoundException
and returns a proper error message to the API client (see step 3).Ok, now on to the details:
Step 1: Configure application.properties
I had to add the following two configuration settings to the project's application.properties
file:
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
This makes sure, the NoHandlerFoundException
is thrown in cases where a client tries to access an URL for which no controller method exists which would be able to handle the request.
Step 2: Create a Class for API Errors
I made a class similar to the one suggested in this article on Eugen Paraschiv's blog. This class represents an API error. This information is sent to the client in the HTTP response body in case of an error.
public class ApiError {
private int code;
private String message;
private Instant timestamp;
public ApiError(int code, String message) {
this.code = code;
this.message = message;
this.timestamp = Instant.now();
}
public ApiError(int code, String message, Instant timestamp) {
this.code = code;
this.message = message;
this.timestamp = timestamp;
}
// Getters and setters here...
}
Step 3: Create / Configure a Global Exception Handler
I use the following class to handle exceptions (for simplicity, I have removed import statements, logging code and some other, non-relevant pieces of code):
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiError noHandlerFoundException(
NoHandlerFoundException ex) {
int code = 1000;
String message = "No handler found for your request.";
return new ApiError(code, message);
}
// More exception handlers here ...
}
Step 4: Write a test
I want to make sure, the API always returns the correct error messages to the calling client, even in the case of failure. Thus, I wrote a test like this:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SprintBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("dev")
public class GlobalExceptionHandlerIntegrationTest {
public static final String ISO8601_DATE_REGEX =
"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$";
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "DEVICE_SCAN_HOSTS")
public void invalidUrl_returnsHttp404() throws Exception {
RequestBuilder requestBuilder = getGetRequestBuilder("/does-not-exist");
mockMvc.perform(requestBuilder)
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code", is(1000)))
.andExpect(jsonPath("$.message", is("No handler found for your request.")))
.andExpect(jsonPath("$.timestamp", RegexMatcher.matchesRegex(ISO8601_DATE_REGEX)));
}
private RequestBuilder getGetRequestBuilder(String url) {
return MockMvcRequestBuilders
.get(url)
.accept(MediaType.APPLICATION_JSON);
}
The @ActiveProfiles("dev")
annotation can be left away. I use it only as I work with different profiles. The RegexMatcher
is a custom Hamcrest matcher I use to better handle timestamp fields. Here's the code (I found it here):
public class RegexMatcher extends TypeSafeMatcher<String> {
private final String regex;
public RegexMatcher(final String regex) {
this.regex = regex;
}
@Override
public void describeTo(final Description description) {
description.appendText("matches regular expression=`" + regex + "`");
}
@Override
public boolean matchesSafely(final String string) {
return string.matches(regex);
}
// Matcher method you can call on this matcher class
public static RegexMatcher matchesRegex(final String string) {
return new RegexMatcher(regex);
}
}
Some further notes from my side:
@EnableWebMvc
annotation. This was not necessary in my case.I think ResponseEntityExceptionHandler
meets your requirements. A sample piece of code for HTTP 400:
@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentNotValidException.class,
HttpRequestMethodNotSupportedException.class})
public ResponseEntity<Object> badRequest(HttpServletRequest req, Exception exception) {
// ...
}
}
You can check this post
For REST controllers, I would recommend to use Zalando Problem Spring Web
.
https://github.com/zalando/problem-spring-web
If Spring Boot aims to embed some auto-configuration, this library does more for exception handling. You just need to add the dependency:
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web</artifactId>
<version>LATEST</version>
</dependency>
And then define one or more advice traits for your exceptions (or use those provided by default)
public interface NotAcceptableAdviceTrait extends AdviceTrait {
@ExceptionHandler
default ResponseEntity<Problem> handleMediaTypeNotAcceptable(
final HttpMediaTypeNotAcceptableException exception,
final NativeWebRequest request) {
return Responses.create(Status.NOT_ACCEPTABLE, exception, request);
}
}
Then you can defined the controller advice for exception handling as:
@ControllerAdvice
class ExceptionHandling implements MethodNotAllowedAdviceTrait, NotAcceptableAdviceTrait {
}
Source: Stackoverflow.com