-
Couldn't load subscription status.
- Fork 38.8k
Description
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;