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?
Using Spring Boot @ExceptionHandler Annotation: @ExceptionHandler annotation provided by Spring Boot can be used to handle exceptions in particular Handler classes or Handler methods. Any method annotated with this is automatically recognized by Spring Configuration as an Exception Handler Method.
To deal with exceptions, the recommended practice is to follow the sequence outlined below: Determine whether the REST API request succeeded or failed, based on the HTTP status response code returned. If the REST API request failed and the response is application/json, serialize the model.
Spring provides a very useful way to handle exceptions using ControllerAdvice. We will be implementing a ControlerAdvice class which will handle all exceptions thrown by the controller class. Exceptions thrown by a Controller method is mapped to the ControllerAdvice method using @ExceptionHandler annotations.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With