2

Let's say we have @RestControllerAdvice-annotated class like this:

@RestControllerAdvice
public class RestResponseExceptionHandler {

  @ExceptionHandler(MyBusinessException .class)
  public ResponseEntity<ErrorResponse> handleMyBusinessException (MyBusinessException ex) {
      return createResponseEntity(ex, ex.getErrorCode());
  }

  @ExceptionHandler({IllegalArgumentException.class, ValidationException.class, DataIntegrityViolationException.class})
  public ResponseEntity<ErrorResponse> handleInvalidPropertyException(RuntimeException ex) {
      return createResponseEntity(ex, ErrorCode.DATA_INVALID);
  }

  [...]

  @ExceptionHandler(RuntimeException.class)
  public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException ex) {
        return createResponseEntity(ex, ErrorCode.UNKNOWN);
  }

  @ExceptionHandler(Exception.class)
  public ResponseEntity<ErrorResponse> handleException(Exception ex) {
      return createResponseEntity(ex, ErrorCode.UNKNOWN);
  }

  @ExceptionHandler(WrapperException .class)
  public ResponseEntity<ErrorResponse> handleWrapperException (WrapperException ex) {
      Exception exception = ex.getWrappedException();
      // re-dispatch exception here
  }
}

For a known WrapperException, is it possible to somehow re-dispatch the wrapped exception?

I tried several things, e.g. rethrowing the wrapped excption or explicitly call a custom method of our ErrorController and re-throw the exception there, but so far no luck.

Puce
  • 37,247
  • 13
  • 80
  • 152
  • How about reflection? Getting all methods annotated with `@ExceptionHandler` and map exception class/es from annotation to method itself - `Map, Method>`. Then get correct method using getWrappedException().class and invoke it with the wrapped exception as parameter. If all methods have single parameter, the caught exception, there shouldn't be problems with the invocation. Can we think about solution in this direction? – Chaosfire Jan 24 '22 at 11:36
  • @Chaosfire In general possible, but again, that is what Spring does already, and I'm looking for a way to re-use that funtionality, if possible. – Puce Jan 24 '22 at 11:40

3 Answers3

1

Why would you want to re-throw which may create unnecessary branching. You can do a conditional check and call the appropriate exception handler method as below.

@ExceptionHandler(WrapperException .class)
public ResponseEntity<ErrorResponse> handleWrapperException (WrapperException ex) {
  Exception exception = ex.getWrappedException();
  if (exception instanceof MyBusinessException) {
      return handleMyBusinessException((MyBusinessException) exception);
  }
  return //Default
 } 
shazin
  • 21,379
  • 3
  • 54
  • 71
  • 1
    That's what we did so far, but as the error handling is growing, we would basically have to test all ExceptionHandler methods, which is what Spring already does. This is quite error prone (a dev might forget to add a new ExceptionHandler method to the if-else statement). So I try to find a way to re-use the ExceptionHandler method dispatching of Spring. – Puce Jan 24 '22 at 11:11
  • @Puce In that case you can get rid of the WrapperException throwing all together and throw root exception' – shazin Jan 24 '22 at 11:15
  • If we could, we would, of course. "WrapperExceptions" typically are used in places where checked exceptions cannot be thrown. We use a custom one as well as wrapper exceptions from frameworks such as `Exceptions.propagate` of WebFlux (though this one is handled in a special way). – Puce Jan 24 '22 at 11:29
  • @Puce I couldn't understand your use-case. Even if Spring automatically handled rethrowing of `ex.getWrappedException()`, when a new exception is added, dev can still forget to add the handler for that. – Smile Jan 24 '22 at 12:01
1

I think i figured the spring-ish way to do it. You can autowire in your RestResponseExceptionHandler bean of this type - HandlerExceptionResolver. Spring autoconfigures few of those for you, i managed to make it work with this one - handlerExceptionResolver. Something like this:

@RestControllerAdvice
public class ErrorHandler {

    private static final String BEAN1 = "handlerExceptionResolver";
    private static final String BEAN2 = "customResolver";

    private final HandlerExceptionResolver resolver;

    @Autowired
    public ErrorHandler(@Qualifier(BEAN2) HandlerExceptionResolver resolver) {
        this.resolver = resolver;
    }

    @ExceptionHandler(WrapperException.class)
    public void exception(WrapperException exception, HttpServletRequest request, HttpServletResponse response) {
        Exception wrappedException = exception.getWrappedException();
        //this will dispatch the handling to the handler for the wrapped exception
        this.resolver.resolveException(request, response, null, wrappedException);
    }

    @ExceptionHandler(IndexOutOfBoundsException.class)
    public ResponseEntity<Object> indexOutOfBoundsException() {
        return ResponseEntity.of(Optional.of("IndexOutOfBoundsException"));
    }

    @ExceptionHandler(NullPointerException.class)
    public ResponseEntity<Object> runtimeException() {
        return ResponseEntity.of(Optional.of("NullPointerException"));
    }

    @ExceptionHandler(IOException.class)
    public ResponseEntity<Object> ioException() {
        return ResponseEntity.of(Optional.of("IOException"));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Object> illegalArgumentException() {
        return ResponseEntity.of(Optional.of("IllegalArgumentException"));
    }
}

EDIT: Changed to autowire custom defined bean, the name of the spring autoconfigured bean is still there.

You will have to use the request and response in the wrapped exception handler, otherwise won't be able to handle it. When you call resolveException() with the wrapped one, it will reroute it to the correct handler for the wrapped exception. I did some testing using a controller to throw exceptions like this one, and everything was resolved correctly.

@Controller
public class ExcController {

    @GetMapping("/thr")
    public String throwExc() throws Exception {
        throw new Exception(new NullPointerException());
    }

    @GetMapping("/thr2")
    public String throwExc2() throws Exception {
        throw new IOException();
    }
}

EDIT: Found another implementation of HandlerExceptionResolver, which works - ExceptionHandlerExceptionResolver. To avoid circular dependencies of already existing beans, declare custom resolver to handle WrapperExceptions:

@Configuration
public class ExceptionResolverConfig {

    @Bean
    public HandlerExceptionResolver customResolver() {
        return new ExceptionHandlerExceptionResolver();
    }
}

Then autowire this bean in ErrorHandler. ExceptionHandlerExceptionResolver worked for me without any additional configuration.

Chaosfire
  • 4,818
  • 4
  • 8
  • 23
  • Thanks, I will try it. – Puce Jan 24 '22 at 13:59
  • I got the following error during start-up: "The dependencies of some of the beans in the application context form a cycle:" showing my controller advice class and: `handlerExceptionResolver defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]` – Puce Jan 25 '22 at 13:35
  • With field injection instead of constructor injection it works, thanks! – Puce Jan 25 '22 at 14:42
  • @Puce Glad to hear it! Still, for completeness, i editted the answear with option to plug your custom bean to avoid any circular dependencies. – Chaosfire Jan 25 '22 at 15:15
  • Unfortunately, `ExceptionHandlerExceptionResolver` does not work. In one case I get a `org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation` and in another the exception gets not dispatched at all. – Puce Jan 26 '22 at 16:31
  • @Puce I am totally baffled. Sounds as if `exceptionHandlerAdviceCache` is not getting initialized, but if you decalre `ExceptionHandlerExceptionResolver` as a bean, spring should be taking care of this. Which version of spring boot are you using? Perhaps earlier versions need manual initialization. – Chaosfire Jan 27 '22 at 16:47
  • Tested with Spring Boot v2.5.6 (Spring v5.3.12) – Puce Jan 27 '22 at 17:22
  • @Puce Unfortunately, no matter what i do, i can't reproduce the problems you mentioned. Everything works fine for me, tested with v2.5.6 as well. You could try putting `@DependsOn("youControllerAdviceBeanName")` on the new bean declaration, to make sure controller advice is instantiated first, but to be honest i don't have high hopes for that working. – Chaosfire Jan 29 '22 at 09:05
0

As of Spring 5.3, @ExceptionHandler already looks into the cause exceptions when trying to find a match. So if you are on a recent Spring version, you can just remove your @ExceptionHandler(WrapperException.class) method, and it should just work as you expect.

The decision whether Spring matches the top-level (root) exception or the wrapped exception is a bit involved, and is fully explained in the manual: https://docs.spring.io/spring-framework/docs/5.3.15/reference/html/web.html#mvc-ann-exceptionhandler

The simplest way to get the right order working is to write the most unspecific exception handlers (i.e. for Exception and RuntimeException and similar) in their own @ControllerAdvice class and define a low precedence for this bean, e.g. with @Order(Ordered.LOWEST_PRECEDENCE). The handlers for the specific wrapped exceptions should then be defined in one or multiple beans of higher precedence.

Since this seems to have quite a high regression potential (e.g. by refactoring and shuffling things around), it is important to have some kind of test or tests for the correct precedence of exception handling.

jhyot
  • 3,733
  • 1
  • 27
  • 44
  • Wouldn't it go the hierarchy first and hit the exception handler method of RuntimeException or Exception, before looking at the cause? – Puce Jan 24 '22 at 13:56
  • Apparently not according to my tests. My codebase has that exact scenario. You should write your own tests to be sure though. I actually couldn't find the precedence rules by a quick Google search. They are not mentioned in the Javadoc. Maybe in the Spring manual. – jhyot Jan 24 '22 at 15:09
  • Unfortunately, I cannot confirm this. If WrapperException extends RuntimeException , then the exception handler method for RuntimeException hits and not the exception handler method for the cause exception – Puce Jan 25 '22 at 13:18
  • Tested with Spring Boot v2.5.6 (Spring v5.3.12) – Puce Jan 25 '22 at 14:44
  • Ok I think I got it, it depends on the precendece of the ControllerAdvices. I also found the link for the full explanation of the priorities. I edited my answer to include the manual link and expand on the needed setup. – jhyot Jan 25 '22 at 17:30