spring-framework icon indicating copy to clipboard operation
spring-framework copied to clipboard

Request to provide Primary Exception Handler to resolve `Ambiguous @ExceptionHandler method mapped for [Exception]`

Open rsampaths16 opened this issue 6 months ago • 0 comments

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 -

       @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:

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;

rsampaths16 avatar Aug 16 '24 17:08 rsampaths16