Spring Boot REST service exception handling

318,958

Solution 1

New answer (2016-04-20)

Using Spring Boot 1.3.1.RELEASE

New Step 1 - It is easy and less intrusive to add the following properties to the application.properties:

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

Much easier than modifying the existing DispatcherServlet instance (as below)! - JO'

If working with a full RESTful Application, it is very important to disable the automatic mapping of static resources since if you are using Spring Boot's default configuration for handling static resources then the resource handler will be handling the request (it's ordered last and mapped to /** which means that it picks up any requests that haven't been handled by any other handler in the application) so the dispatcher servlet doesn't get a chance to throw an exception.


New Answer (2015-12-04)

Using Spring Boot 1.2.7.RELEASE

New Step 1 - I found a much less intrusive way of setting the "throExceptionIfNoHandlerFound" flag. Replace the DispatcherServlet replacement code below (Step 1) with this in your application initialization class:

@ComponentScan()
@EnableAutoConfiguration
public class MyApplication extends SpringBootServletInitializer {
    private static Logger LOG = LoggerFactory.getLogger(MyApplication.class);
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(MyApplication.class, args);
        DispatcherServlet dispatcherServlet = (DispatcherServlet)ctx.getBean("dispatcherServlet");
        dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
    }

In this case, we're setting the flag on the existing DispatcherServlet, which preserves any auto-configuration by the Spring Boot framework.

One more thing I've found - the @EnableWebMvc annotation is deadly to Spring Boot. Yes, that annotation enables things like being able to catch all the controller exceptions as described below, but it also kills a LOT of the helpful auto-configuration that Spring Boot would normally provide. Use that annotation with extreme caution when you use Spring Boot.


Original Answer:

After a lot more research and following up on the solutions posted here (thanks for the help!) and no small amount of runtime tracing into the Spring code, I finally found a configuration that will handle all Exceptions (not Errors, but read on) including 404s.

Step 1 - tell SpringBoot to stop using MVC for "handler not found" situations. We want Spring to throw an exception instead of returning to the client a view redirect to "/error". To do this, you need to have an entry in one of your configuration classes:

// NEW CODE ABOVE REPLACES THIS! (2015-12-04)
@Configuration
public class MyAppConfig {
    @Bean  // Magic entry 
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet ds = new DispatcherServlet();
        ds.setThrowExceptionIfNoHandlerFound(true);
        return ds;
    }
}

The downside of this is that it replaces the default dispatcher servlet. This hasn't been a problem for us yet, with no side effects or execution problems showing up. If you're going to do anything else with the dispatcher servlet for other reasons, this is the place to do them.

Step 2 - Now that spring boot will throw an exception when no handler is found, that exception can be handled with any others in a unified exception handler:

@EnableWebMvc
@ControllerAdvice
public class ServiceExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(Throwable.class)
    @ResponseBody
    ResponseEntity<Object> handleControllerException(HttpServletRequest req, Throwable ex) {
        ErrorResponse errorResponse = new ErrorResponse(ex);
        if(ex instanceof ServiceException) {
            errorResponse.setDetails(((ServiceException)ex).getDetails());
        }
        if(ex instanceof ServiceHttpException) {
            return new ResponseEntity<Object>(errorResponse,((ServiceHttpException)ex).getStatus());
        } else {
            return new ResponseEntity<Object>(errorResponse,HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @Override
    protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        Map<String,String> responseBody = new HashMap<>();
        responseBody.put("path",request.getContextPath());
        responseBody.put("message","The URL you have reached is not in service at this time (404).");
        return new ResponseEntity<Object>(responseBody,HttpStatus.NOT_FOUND);
    }
    ...
}

Keep in mind that I think the "@EnableWebMvc" annotation is significant here. It seems that none of this works without it. And that's it - your Spring boot app will now catch all exceptions, including 404s, in the above handler class and you may do with them as you please.

One last point - there doesn't seem to be a way to get this to catch thrown Errors. I have a wacky idea of using aspects to catch errors and turn them into Exceptions that the above code can then deal with, but I have not yet had time to actually try implementing that. Hope this helps someone.

Any comments/corrections/enhancements will be appreciated.

Solution 2

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

Solution 3

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

Solution 4

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:

  • Make sure that the NoHandlerFoundException is thrown if API clients call URLS for which no handler method exists (see Step 1 below).
  • Create a custom error class (in my case ApiError) which contains all the data that should be returned to the API client (see step 2).
  • Create an exception handler which reacts on the NoHandlerFoundException and returns a proper error message to the API client (see step 3).
  • Write a test for it and make sure, it works (see step 4).

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:

  • In many other posts on StackOverflow, people suggested setting the @EnableWebMvc annotation. This was not necessary in my case.
  • This approach works well with MockMvc (see test above).

Solution 5

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

}
Share:
318,958

Related videos on Youtube

ogradyjd
Author by

ogradyjd

Straight up batsh*t insane highly experienced developer.

Updated on January 17, 2020

Comments

  • ogradyjd
    ogradyjd over 4 years

    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?

    • OrangeDog
      OrangeDog about 8 years
      The client is never actually sent a redirect. The redirect is handled internally by the servlet container (e.g. Tomcat).
    • pmorken
      pmorken about 7 years
      Removing the @ResponseStatus annotations on my exception handlers was what I needed; see stackoverflow.com/questions/35563968/…
  • ogradyjd
    ogradyjd about 9 years
    I have seen this code before, and after implementing it, the class did catch exceptions raised in controller requestmapping methods. This still does not catch 404 errors, which are getting handled in the ResourceHttpRequestHandler.handleRequest method, or, if the @EnableWebMvc annotation is used, in DispatcherServlet.noHandlerFound. We want to handle any error, including 404s, but the latest version of Spring Boot seem to be incredibly obtuse on how to do that.
  • ogradyjd
    ogradyjd almost 9 years
    The default DispatcherServlet is hardcoded to do the redirect with MVC rather than throw an exception when a request for a non-existent mapping is received - unless you set the flag as I did in the post above.
  • ogradyjd
    ogradyjd almost 9 years
    Also, the reason we implemented the ResponseEntityExceptionHandler class is so we could control the format of the output and log error stack traces to a NoSQL solution and then send a client-safe error message.
  • wwadge
    wwadge over 8 years
    instead of creating a new dispatcher servlet bean you can flip the flag in a post processor: YourClass implements BeanPostProcessor { ... `public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException{ if (bean instanceof DispatcherServlet) { // otherwise we get a 404 before our exception handler kicks in ((DispatcherServlet) bean).setThrowExceptionIfNoHandlerFound(true); } return bean; } public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; }
  • Ian Gilham
    Ian Gilham over 8 years
    I have this problem but customizing the DispatcherServlet doesn't work for me. Is there any additional magic required for Boot to use this extra bean and config?
  • FrVaBe
    FrVaBe over 8 years
    @IanGilham I too do not get this to work with Spring Boot 1.2.7. I even do not get any @ExceptionHandler method called when placing it into the @ControllerAdvice class although they properly work if placed in the @RestController class. @EnableWebMvc is on the @ControllerAdvice and the @Configuration (I tested every combination) class. Any idea or working example? // @Andy Wilkinson
  • Ian Gilham
    Ian Gilham over 8 years
    @FrVaBe it sounds like the @ControllerAdvice might not be picked up by the configuration auto-scan defined on the application class.
  • FrVaBe
    FrVaBe over 8 years
    @IanGilham Yes, but I haven no idea why. Is there any full working simple example out there?
  • Ian Gilham
    Ian Gilham over 8 years
    @FrVaBe there are only limited examples in the spring boot docs. Just make sure anything using spring annotations is in the same package as the main class or a sub package with the same root.
  • Andy Wilkinson
    Andy Wilkinson over 8 years
    Using @EnableWebMvc almost certainly isn't what you want. It turns off all of Spring Boot's auto-configuration of Spring MVC. I don't understand the concerns about redirects and forwards in the question as the client doesn't see any of that. Implementing a custom ErrorController is almost certainly a better way to tackle this problem.
  • FrVaBe
    FrVaBe over 8 years
    Whoever reads this question and answer should have a look at the corresponding SpringBoot Issue on github.
  • A Gupta
    A Gupta over 8 years
    I am using Spring boot 1.3.0.RELEASE and its only working for the exceptions which are being thrown from Controllers. Its not handling other exceptions which are thrown from outside controller. e.g. Filters etc. Any clue ? @ogradyjd
  • ogradyjd
    ogradyjd over 8 years
    Not sure @agpt. I have an internal project that I can move up to 1.3.0 and see what the effect is on my setup and let you know what I find.
  • A Gupta
    A Gupta over 8 years
    @ogradyjd Thank you !
  • user3748908
    user3748908 over 8 years
    @agpt Did you find a solution? It also doesn't work for me with 1.3.0. It still maps for instance persistence exceptions to /error
  • A Gupta
    A Gupta over 8 years
    @user3748908 no not yet. I guess best solution would be to go for AOP based solution. writing own pointcut etc
  • fiskra
    fiskra about 7 years
    That's really helpful, thank you. But I didn't get why we need to ` @EnableWebMvc ` with ` spring.mvc.throw-exception-if-no-handler-found=true ` . My expectation was to handle all exceptions via @RestControllerAdvice without additional configuration. What am I missing here?
  • chrisinmtown
    chrisinmtown almost 7 years
    Please note that property setting "spring.resources.add-mappings=false" disables Swagger.
  • PGMacDesign
    PGMacDesign about 6 years
    This solved the problem for me. Just to add, I was missing the @ RestControllerAdvice annotation so I added that along with the @ ControllerAdvice annotation so that it would handle all and that did the trick.
  • Paramesh Korrakuti
    Paramesh Korrakuti over 5 years
    I wrote the same way to handle HttpRequestMethodNotSupportedException and plugin the same jar in multiple micro-services, for some business purpose we need to respond micro-service alias name in the response. is there any way we can get the underlying micro-service name / controller name ? I know HandlerMethod will provide the java method name from where the exception is originated. But here, none of the methods received the request, hence HandlerMethod won't be initialized. So is there any solution to solve this?
  • Jorge Tovar
    Jorge Tovar almost 5 years
    Controller advice is a good approach, but always remember that exceptions are not part of the flow they must occur in exceptional cases!