Affects: spring-boot-3.3.0+

Initial Context

The ResponseEntityExceptionHandler is a really good utility class as it provides a default exception handler for many exceptions out of the box.

Extending it with a CustomGlobalExceptionHandler is a good place to incrementally adopt and handle exceptions.

I've followed a similar pattern of handling exceptions in the extended class

@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {
  @ExceptionHandler(
      value = {
        MyException1.class,
        MyException2.class,
        // ...
        MyExceptionN.class,
      })
    public final ResponseEntity<Object> handleMyException(Exception ex, WebRequest request) throws Exception {
      // ... do some thing, ex: setting common headers

      return switch(ex) {
        case MyException1 subEx -> handleMyException1(subEx, headers, myStatus, request);
        case MyException2 subEx -> handleMyException2(subEx, headers, myStatus, request);
        // ...
        case MyExceptionN subEx -> handleMyExceptionN(subEx, headers, myStatus, request);
        default -> throw ex;
      };
    }

  protected ResponseEntity<object> handleMyException1(MyException1 ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
    // ... do something, ex: create a problem detail
    return handleExceptionInternal(ex, problem, headers, status, request);
  }

  protected ResponseEntity<object> handleMyException2(MyException2 ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
    // ... do something, ex: create a problem detail
    return handleExceptionInternal(ex, problem, headers, status, request);
  }

  // ...

  protected ResponseEntity<object> handleMyExceptionN(MyExceptionN ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
    // ... do something, ex: create a problem detail
    return handleExceptionInternal(ex, problem, headers, status, request);
  }
}

The Problem

The problem occurs when both the extending class & base ResponseEntityExceptionHandler class handle the same exception. An Ambiguous @ExceptionHandler method mapped for [Exception] error is raised and the spring application closes.

The Workaround

There does exist a workaround for this as mentioned in https://stackoverflow.com/a/51993609/4239690

The ambiguity is because you have the same method - @ExceptionHandler in both the classes - ResponseEntityExceptionHandler, MethodArgumentNotValidException. You need to write the overridden method as follows to get around this issue - java @Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String errorMessage = ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); List<String> validationList = ex.getBindingResult().getFieldErrors().stream().map(fieldError->fieldError.getDefaultMessage()).collect(Collectors.toList()); LOGGER.info("Validation error list : "+validationList); ApiErrorVO apiErrorVO = new ApiErrorVO(errorMessage); apiErrorVO.setErrorList(validationList); return new ResponseEntity<>(apiErrorVO, status); }

The Pain Point

If we have a common Initialisation logic, or other before/after logic, we're having to having to do it in multiple locations

    @ExceptionHandler(value = {...})
    public final ResponseEntity<Object> handleMyException(Exception ex, WebRequest request) throws Exception {
      // ... do some thing, ex: setting common headers
    }

    @Override
    protected ResponseEntity<Object> handleSimilarExceptionAsParent(Exception ex, WebRequest request) throws Exception {
      // ... do similar things as in `handleMyException
    }
 ```

 This could provide one way of resistance to incremental replacement of default error handlers.

 ### Possible Enhancements
 These are few possible enhancements I can think about

 #### Mark one `ExceptionHandler` as `Primary`
 Have an annotation `PrimaryExceptionHandler`, which is the first to handle an exception, and then fallback to other `ExceptionHandler`

 #### Introduce conditional ExceptionHandler
 For each exception handled by `ResponseEntityExceptionHandler::handleException` conditionally add exception to the list of handled exceptions.

 #### Delegate to central exception handler and use `handleException` as fallback
 Have a delegate to a central exception handler and handle in `handleException` as a fallback.
 Ex:
 ```java
 protected ResponseEntity<Object> handleExceptionDelegate(Exception ex, WebRequest request) throws Exception {
   throw ex;
 }

 public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
   try {
     return handleExceptionDelegate(ex, request);
   } catch (Exception caught) {
     if (!ex.equals(caught) {
       throw caught;
     }
   }
   // ... continue processing as usual
 }
 ```
 Note that this delegate is at a higher level than `handleExceptionInternal`;

**Comment From: rstoyanchev**

The workaround you mention is not a workaround, but what is expected. If you want to customize the handling of a built-in exception, you override the provided protected method. Is there a reason such common initialization logic cannot be invoked from there? In other words:

```java
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {

        // ... do some thing, ex: setting common headers

        // remaining logic to handle exception
    }

I notice that you also create your own ApiErrorVO for the body. That means you're using your own error response format as opposed to the RFC 9457 format, which is what ResponseEntityExceptionHandler uses by default for the built-in exceptions. I am guessing that you want the same ApiErrorVO format for built-in exceptions as well, which means that you must either override the individual protected methods for each built-in exception, or override handleExceptionInternal in order to apply the ApiErrorVO format to all exceptions from a single place. In other words, either way you have to work with these protected methods, that's what they are there for.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: spring-projects-issues

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.