spring-framework
spring-framework copied to clipboard
Request to provide Primary Exception Handler to resolve `Ambiguous @ExceptionHandler method mapped for [Exception]`
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
;