I'm trying to figure out the simplest way to take control over the 404 Not Found handler of a basic Spring Boot RESTful service such as the example provided by Spring:
https://spring.io/guides/gs/rest-service/
Rather than have it return the default Json output:
{
"timestamp":1432047177086,
"status":404,
"error":"Not Found",
"exception":"org.springframework.web.servlet.NoHandlerFoundException",
"message":"No handler found for GET /aaa, ..."
}
I'd like to provide my own Json output.
By taking control of the DispatcherServlet and using DispatcherServlet#setThrowExceptionIfNoHandlerFound(true), I was able to make it throw an exception in case of a 404 but I can't handle that exception through a #ExceptionHandler, like I would for a MissingServletRequestParameterException. Any idea why?
Or is there a better approach than having a NoHandlerFoundException thrown and handled?
It works perfectly Fine.
When you are using SpringBoot, it does not handle (404 Not Found) explicitly; it uses WebMvc error response. If your Spring Boot should handle that exception, then you should do some hack around Spring Boot. For 404, the exception class is NoHandlerFoundException; if you want to handle that exception in your #RestControllerAdvice class, you must add #EnableWebMvc annotation in your Application class and set setThrowExceptionIfNoHandlerFound(true); in DispatcherServlet. Please refer to the following code:
#SpringBootApplication
#EnableWebMvc
public class Application {
#Autowired
private DispatcherServlet servlet;
public static void main(String[] args) throws FileNotFoundException, IOException {
SpringApplication.run(Application.class, args);
}
#Bean
public CommandLineRunner getCommandLineRunner(ApplicationContext context) {
servlet.setThrowExceptionIfNoHandlerFound(true);
return args -> {};
}
}
After this you can handle NoHandlerException in your #RestControllerAdvice class
#RestControllerAdvice
public class AppException {
#ExceptionHandler(value={NoHandlerFoundException.class})
#ResponseStatus(code=HttpStatus.BAD_REQUEST)
public ApiError badRequest(Exception e, HttpServletRequest request, HttpServletResponse response) {
e.printStackTrace();
return new ApiError(400, HttpStatus.BAD_REQUEST.getReasonPhrase());
}
}
I have created ApiError class to return customized error response
public class ApiError {
private int code;
private String message;
public ApiError(int code, String message) {
this.code = code;
this.message = message;
}
public ApiError() {
}
//getter & setter methods...
}
According to the Spring documentation appendix A. there is a boolean property called spring.mvc.throw-exception-if-no-handler-found which can be used to enable throwing NoHandlerFoundException. Then you can create exception handler like any other.
#RestControllerAdvice
class MyExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(MyExceptionHandler.class);
#ResponseStatus(HttpStatus.NOT_FOUND)
#ExceptionHandler(NoHandlerFoundException.class)
public String handleNoHandlerFoundException(NoHandlerFoundException ex) {
log.error("404 situation detected.",ex);
return "Specified path not found on this server";
}
}
#ExceptionHandler itself without #ControllerAdvice (or #RestControllerAdvice) can't be used, because it's bound to its controller only.
In short, the NoHandlerFoundException is thrown from the Container, not from your application within your container. Therefore your Container has no way of knowing about your #ExceptionHandler as that is a Spring feature, not anything from the container.
What you want is a HandlerExceptionResolver. I had the very same issue as you, have a look at my solution over there: How to intercept "global" 404s on embedded Tomcats in spring-boot
The #EnableWebMvc based solution can work, but it might break Spring boot auto configurations.
The solution I am using is to implement ErrorController:
#RestController
#RequiredArgsConstructor
public class MyErrorController implements ErrorController {
private static final String ERROR_PATH = "/error";
#NonNull
private final ErrorAttributes errorAttributes;
#RequestMapping(value = ERROR_PATH)
Map<String, Object> handleError(WebRequest request) {
return errorAttributes.getErrorAttributes(request, false);
}
#Override
public String getErrorPath() {
return ERROR_PATH;
}
}
The solution for this problem is:
Configure DispatcherServlet to throw and exception if it doesn't find any handlers.
Provide your implementation for the exception that will be thrown from DispatcherServlet, for this case is the NoHandlerFoundException.
Thus, in order to configure DispatcherServlet you may use properties file or Java code.
Example for properties.yaml,
spring:
mvc:
throw-exception-if-no-handler-found: true
Example for properties.properties,
spring.mvn.throw-exception-if-no-handler-found=true
Example for Java code, we just want to run the command servlet.setThrowExceptionIfNoHandlerFound(true); on startup, I use the InitializingBean interface, you may use another way. I found a very well written guide to run logic on startup in spring from baeldung.
#Component
public class WebConfig implements InitializingBean {
#Autowired
private DispatcherServlet servlet;
#Override
public void afterPropertiesSet() throws Exception {
servlet.setThrowExceptionIfNoHandlerFound(true);
}
}
Be careful! Adding #EnableWebMvc disables autoconfiguration in Spring Boot 2, meaning that if you use the annotation #EnableWebMvc then you should use the Java code example, because the spring.mvc.* properties will not have any effect.
After configuring the DispatcherServlet, you should override the ResponseEntityExceptionHandler which is called when an Exception is thrown. We want to override the action when the NoHandlerFoundException is thrown, like the following example.
#ControllerAdvice
public class MyApiExceptionHandler extends ResponseEntityExceptionHandler {
#Override
public ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String responseBody = "{\"errormessage\":\"WHATEVER YOU LIKE\"}";
headers.add("Content-Type", "application/json;charset=utf-8");
return handleExceptionInternal(ex, responseBody, headers, HttpStatus.NOT_FOUND, request);
}
}
Finally, adding a break point to method handleException of ResponseEntityExceptionHandler might be helpful for debugging.
Related
I'm experimenting with the custom validator annotation with spring boot in my rest application.
here is the sample code from the validator, other requisite classes are present:
public class CustomerValidator implements ConstraintValidator<CustomerValidation, Customer>
{
public boolean isValid(Customer value, ConstraintValidatorContext cxt) {
if(value.getItemList.size() == 0) {
throw new CustomerOrderException();
}
if(value.getBalance() < value.getCost()) {
throw new InsufficientBalanceException();
}
return true;
}
}
I made a class annotated with controller advice and exception handler but it doesn't seem to intercept the exception thrown from the validator.
Ok I got it, I extended my controller advice class with ResponseEntityExceptionHandler and did the usual ExceptionHandler annotation specifying my custom exceptions.
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(value={CustomerOrderException.class})
protected ResponseEntity<Object> handleException(CustomerOrderException ex, WebRequest request) {
//return response entity
}
}
I'm implementing an global exception handling using ControllerAdvice and have trouble having it working with Spring Boot 2.4.
I have a class SecurityExceptionTranslater that is annotated with #ControllerAdvice and implements ExceptionTranslater where ExceptionTranslater is an custom interface.
SecurityExceptionTranslater is an managed bean introduced in my #Configuration configuration
#Configuration(proxyBeanMethods = false)
#ConditionalOnWebApplication
#AutoConfigureAfter(SecurityAutoConfiguration.class)
#AutoConfigureBefore(WebMvcAutoConfiguration.class)
public class SecurityAutoConfiguration {
#Bean
#ConditionalOnMissingBean(ExceptionTranslater.class)
public ExceptionTranslater exceptionTranslater() {
return new SecurityExceptionTranslater();
}
#Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, SecurityWebSupport support) throws Exception {
return http.exceptionHandling()
.authenticationEntryPoint(support)
.accessDeniedHandler(support)
.and()
.build();
}
#Bean
public SecurityWebSupport securityProblemSupport(
#Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver) {
return new SecurityWebSupport(handlerExceptionResolver);
}
}
SecurityWebSupport is used to take over exception handling thrown by Spring Security.
public class SecurityWebSupport implements AuthenticationEntryPoint, AccessDeniedHandler {
private final HandlerExceptionResolver resolver;
public SecurityWebSupport(final HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
#Override
public void commence(final HttpServletRequest request, final HttpServletResponse response,
final AuthenticationException exception) {
resolver.resolveException(request, response, null, exception);
}
#Override
public void handle(final HttpServletRequest request, final HttpServletResponse response,
final AccessDeniedException exception) {
resolver.resolveException(request, response, null, exception);
}
}
As far as I have been inspecting into Spring Boot, exceptionTranslater bean is always created after creation of handlerExceptionResolver defined in WebMvcConfigurationSupport, where Controller beans are collected and manipulated. For that exceptionTranslater has not been instantiated yet and ExceptionTranslater neither is annotated with #ControllerAdive nor contains any #ExceptionHandler annotated method, exceptionTranslater is ignored by handlerExceptionResolver.
However, the configuration works if
Spring Boot is downgraded to 2.3
or SecurityWebSupport bean is removed
or return type of #Bean method exceptionTranslater is modified as concrete type SecurityExceptionTranslater
By doing 1. and 2., exceptionTranslater bean is created before handlerExceptionResolver.
By doing 3., exceptionTranslater is still created after handlerExceptionResolver but it will be proper registered because of #ControllerAdvice annotation and #ExceptionHanlder method.
I'm looking forward to an explanation and a solution.
Thanks all in advance.
I was closer to the answer. After another few hours digging into it, I finally find out the cause.
The root cause is #AutoConfigureAfter(SecurityAutoConfiguration.class).
SecurityAutoConfiguration depends on SecurityFilterChain and in my configuration, my custom SecurityAutoConfiguration depends on SecurityWebSupport, which depends on HandlerExceptionResolver. This is why my ExceptionTranslater is always instantiated before HandlerExceptionResolver.
I ignored the annotation while I was adapting my code to work with Spring Boot 2.4.
Let's say I have the following controller. (Assume that Order.customer is the customer the order belongs to and only they should be able to access it.)
#RestController
#RequestMapping("/orders")
public class OrderController {
#GetMapping
#PostAuthorize("returnObject.customer == authentication.principal")
public Order getOrderById(long id) {
/* Look up the order and return it */
}
}
After looking up the order, #PostAuthorize is used to make sure it belongs to the authenticated customer. If it is not, Spring responds with a 403 Forbidden.
Such an implementation has a problem: Clients can distinguish between orders that do not exist and orders they have no access to. Ideally, 404 should be returned in both cases.
While this could be solved by injecting the Authentication into the handler method and implementing custom logic there, is there any way to achieve this using #PostAuthorize or a similar, declarative API?
You can specify a custom AccessDeniedHandler in your Spring Security configuration.
In the following example, the handler will return a 404 Not Found on an access denied failure.
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling(exceptionHandling -> exceptionHandling
.accessDeniedHandler(accessDeniedHandler())
);
}
#Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
}
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.sendError(HttpStatus.NOT_FOUND.value(), HttpStatus.NOT_FOUND.getReasonPhrase());
}
}
You could try a ControllerAdvice to catch and transform the AccessDeniedException, which PostAuthorize throws.
#RestControllerAdvice
public class ExceptionHandlerController {
#ResponseStatus(HttpStatus.NOT_FOUND)
#ExceptionHandler(AccessDeniedException.class)
public String handleAccessDenied(AccessDeniedException e) {
return "nothing here"; // or a proper object
}
}
I have a simple Spring MVC application in which I want to handle all the unmapped urls using #ControllerAdvice.
Here is the controller:
#ControllerAdvice
public class ExceptionHandlerController {
#ResponseStatus(HttpStatus.NOT_FOUND)
#ExceptionHandler(NoHandlerFoundException.class)
public String handle404() {
return "exceptions/404page";
}
}
Still, every time get Whitelabel Error Page.
I tried using RuntimeException.class, HttpStatus.BAD_REQUEST and extending the class with NoHandlerFoundException but no use.
Any suggestions?
To make it work, you need to set throwExceptionIfNoHandlerFound property on DispecherServlet. You can do that with:
spring.mvc.throwExceptionIfNoHandlerFound=true
in application.properties file, otherwise the requests will always be forwarded to the default servlet and NoHandlerFoundException would ever be thrown.
The problem is, even with this configuration, it doesn't work. From the documentation:
Note that if
org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler is
used, then requests will always be forwarded to the default servlet
and NoHandlerFoundException would never be thrown in that case.
Because Spring Boot uses by default the org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler you'll have to override this using your own WebMvcConfigurer:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
#EnableWebMvc
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
// Do nothing instead of configurer.enable();
}
}
Of course, the above class might be more complicated in your case.
Another way to do so is ErrorController
#Controller
public class MyErrorController implements ErrorController {
#GetMapping("/error")
public ModelAndView errorHandler(HttpServletRequest req) {
// Get status code to determine which view should be returned
Object statusCode = req.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
// In this case, status code will be shown in a view
ModelAndView mav = new ModelAndView("error_default");
mav.addObject("code", statusCode.toString());
return mav;
}
public String getErrorPath() {
return "/error";
}
}
Add the below line in application.properties
spring.mvc.throwExceptionIfNoHandlerFound=true
and #EnableWebMvc with #ControllerAdvice,
Note: add #Override over handleNoHandlerFoundException method
this worked for me!
#EnableWebMvc
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
CustomResponse response = new CustomResponse();
response.setTimestamp(LocalDateTime.now());
response.setMessage(ApplicationErrorCodes.NO_HANDLER_FOUND.getErrorMessage());
response.setStatus(HttpStatus.BAD_REQUEST);
response.setErrorCode(ApplicationErrorCodes.NO_HANDLER_FOUND.getErrorCode());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
}
I've been building a REST API using Spring. I'm using Basic Authentication with Spring Security (3.2) and I'm having an issue where any unauthenticated request causes a 404 error, even with an implementation of AuthenticationEntryPoint (regardless, Spring should give a 401 as far as I am aware by default). Requesting the resource in my browser, I am not even prompted for credentials. Here's a screenshot of the problem:
After reading the documentation and a number of tutorials on the subject, I can't seem to find where I've gone wrong. The only thing I can imagine is happening is some exception that's being caught.
Spring Security configuration:
#Slf4j
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String REALM_NAME = "Autopulse API";
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// Set to stateless authentication.
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.csrf().disable();
httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
httpSecurity.userDetailsService(userDetailsService);
httpSecurity.authorizeRequests()
.antMatchers("/make/private").authenticated();
httpSecurity.httpBasic().realmName(REALM_NAME);
}
}
Authentication Entry Point:
#Slf4j
#Component
public class HttpBasicAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
Controller:
#Slf4j
#RestController
#RequestMapping("/make")
public class MakeController {
#RequestMapping(value = "/private", method = RequestMethod.GET)
public String getPrivateStuff() {
return "private things!";
}
}
When I provide valid user credentials in the Authorization header, I can see the protected resource ("private things!"), however if I do not provide an Authorization header, or I enter invalid credentials, I simply get the 404 error. I can attach my user details service and user details classes if required.
I figured it out. The problem came down to Spring and exception handling. I had a class called ExceptionController that looked like:
#Slf4j
#RestController
#ControllerAdvice
public class ExceptionController implements ErrorController {
// #ExceptionHandler methods here.
#Override
public String getErrorPath() {
return null;
}
}
It turns out that, by implementing ErrorController, I was handing control to this class AFTER the authentication entry point where Spring could not find the appropriate method and would eventually throw the unwanted 404 error. The fix was to remove those details:
#Slf4j
#RestController
#ControllerAdvice
public class ExceptionController {
// #ExceptionHandler methods here.
}