0

I have a simple Spring Boot 2.1 application with a Spring Interceptor and @RestControllerAdvice.

My requirement is to have the Spring Interceptor be called for all situations, including when an exception occurs.

For custom exceptions, the Interceptor handler methods do get called, e.g. preHandle() and afterCompletion(). However, for exceptions handled by ResponseEntityExceptionHandler, the Spring Interceptor does not get called (I need ResponseEntityExceptionHandler's methods to create a custom ResponseBody to send back, however, I also need to trigger Interceptor's afterCompletion() for auditing purposes).

For instance, if a REST request is made with PATCH HTTP method, it only executes PersonControllerExceptionHandler.handleHttpRequestMethodNotSupported() and no PersonInterceptor is invoked.

Exception Handler:

@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class PersonControllerExceptionHandler extends ResponseEntityExceptionHandler {

    private final static Logger LOGGER = LoggerFactory.getLogger(PersonControllerExceptionHandler.class);

    @ExceptionHandler(value = {PersonException.class })
    public ResponseEntity<Object> handlePersonException(PersonException exception) {
        LOGGER.info("Person exception occurred");

        return new ResponseEntity<Object>(new Person("Bad Age", -1),
                HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(value = {Exception.class })
    public ResponseEntity<Object> handleException(Exception exception) {
        LOGGER.info("Exception occurred");

        return new ResponseEntity<Object>(new Person("Unknown Age", -100),
                HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @Override
    public ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
                                                                      HttpHeaders headers,
                                                                      HttpStatus status,
                                                                      WebRequest request) {
        LOGGER.info("handleHttpRequestMethodNotSupported()...");
        return new ResponseEntity<>(new Person("Argh!", 900), HttpStatus.METHOD_NOT_ALLOWED);
    }

}

The Interceptor:

@Order(Ordered.HIGHEST_PRECEDENCE)
public class PersonInterceptor extends HandlerInterceptorAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(PersonInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        LOGGER.info("PersonInterceptor#preHandler()...");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        LOGGER.info("PersonInterceptor#postHandler()...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        LOGGER.info("PersonInterceptor#afterCompletion()...");

        if (ex != null) {
            LOGGER.error("afterCompletion(): An exception occurred", ex);
        }
    }
}

Registering the Interceptor:

@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new PersonInterceptor()).addPathPatterns("/person/*");
    }
}

Controller:

@RestController
@RequestMapping("/")
public class PersonController {

    private final static Logger LOGGER = LoggerFactory.getLogger(PersonController.class);

    @Autowired
    private PersonService personService;

    @GetMapping(path = "/person/{age}", produces = MediaType.APPLICATION_JSON_VALUE)
    public Person getPerson(@PathVariable("age") Integer age) throws PersonException {
        LOGGER.info("Age: {}", age);

        return personService.getPerson(age);
    }
}

Initially I thought it has something to do with @Ordered but trying various scenarios where I give PersonInterceptor a higher precedence than @RestControllerAdvice yields the same undesirable outcome (or vice versa).

After digging into Spring framework, it seems like if a handler is not found, an exception is thrown back to DispacherServlet#doDispatch() which goes into a catch block, and therefore, it skips interceptor mapping process, including the afterCompletion() (I'm using Spring 5.1. as an example to trace the execution path):

  1. DispacherServlet#doDispatch() is called and attempts is made to get the HandlerExecutionChain
  2. I can see there are several HandlerMapping's; the one that fails is RequestMappingHandlerMapping
  3. In AbstractHandlerMapping#getHandler(), it tries to get the handler via AbstractHandlerMethodMapping#getHandlerInternal()
  4. Eventually, AbstractHandlerMethodMapping#lookupHandlerMethod() is called which fails to find a matching pattern due to the fact that there is no PATCH getPerson(), but rather GET getPerson()
  5. At this point, RequestMappingInfoHandlerMapping#handleNoMatch() throws HttpRequestMethodNotSupportedException
  6. This exception bubbles up to DispatcherServlet#doDispatch() exception clause which then processes by the exception resolver that it finds in DispatcherServlet#processHandlerException() (of course, this finds an exception resolver and doesn't throw an exception which might trigger DispatcherServlet#triggerAfterCompletion() when an exception is caught in DispacherServlet#doDispatch() exception clause

Is there something I am missing to trigger the interceptor's afterCompletion() in cases when there is no handler match?

RamPrakash
  • 1,687
  • 3
  • 20
  • 25
Malvon
  • 1,591
  • 3
  • 19
  • 42
  • This is a very old and very broken spring feature, neither RestControllerAdvice, nor ControllerAdvice or ErrorController (deprecated) work as intended when it comes to 404 handling. Sadly, it has always been this signature spring bug. – downvoteit Aug 28 '20 at 11:42
  • Thanks. What do you suggest one should use instead within the Spring framework to handle global and container errors then? And I don't think [ErrorController](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/web/servlet/error/ErrorController.html) has been deprecated. – Malvon Aug 31 '20 at 21:19

0 Answers0