membersound
membersound

Reputation: 86747

How to catch all unhandled exceptions (i.e. without existing @ExceptionHandler) in Spring MVC?

I want to let HandlerExceptionResolver resolve any Exceptions that I don't explicit catch via @ExceptionHandler annotation.

Anyways, I want to apply specific logic on those exceptions. Eg send a mail notification or log additionally. I can achieve this by adding a @ExceptionHandler(Exception.class) catch as follows:

@RestControllerAdvice
public MyExceptionHandler {
    @ExceptionHandler(IOException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Object io(HttpServletRequest req, Exception e) {
        return ...
    }


    @ExceptionHandler(Exception.class)
    public Object exception(HttpServletRequest req, Exception e) {
        MailService.send();
        Logger.logInSpecificWay();

        //TODO how to continue in the "normal" spring way with HandlerExceptionResolver?
    }
}

Problem: if I add @ExceptionHandler(Exception.class) like that, I can catch those unhandled exceptions.

BUT I cannot let spring continue the normal workflow with HandlerExceptionResolver to create the response ModelAndView and set a HTTP STATUS code automatically.

Eg if someone tries a POST on a GET method, spring by default would return a 405 Method not allowed. But with an @ExceptionHandler(Exception.class) I would swallow this standard handling of spring...

So how can I keep the default HandlerExceptionResolver, but still apply my custom logic?

Upvotes: 29

Views: 28313

Answers (4)

Renato Dinhani
Renato Dinhani

Reputation: 36686

I had the same issue and solved it creating a implementation of the interface HandlerExceptionResolver and removing the generic @ExceptionHandler(Exception.class) from the generic handler method. .

It works this way:

Spring will try to handle the exception calling MyExceptionHandler first, but it will fail to find a handler because the annotation was removed from the generic handler. Next it will try other implementations of the interface HandlerExceptionResolver. It will enter this generic implementation that just delegates to the original generic error handler.

After that, I need to convert the ResponseEntity response to ModelAndView using MappingJackson2JsonView because this interface expects a ModelAndView as return type.

@Component
class GenericErrorHandler(
    private val errorHandler: MyExceptionHandler,
    private val objectMapper: ObjectMapper
) : HandlerExceptionResolver {

    override fun resolveException(request: HttpServletRequest, response: HttpServletResponse, handler: Any, ex: Exception): ModelAndView? {
        // handle exception
        val responseEntity = errorHandler.handleUnexpectedException(ex)

        // prepare JSON view
        val jsonView = MappingJackson2JsonView(objectMapper)
        jsonView.setExtractValueFromSingleKeyModel(true) // prevents creating the body key in the response json

        // prepare ModelAndView
        val mv = ModelAndView(jsonView, mapOf("body" to responseEntity.body))
        mv.status = responseEntity.statusCode
        mv.view = jsonView
        return mv
    }
}

Upvotes: 1

membersound
membersound

Reputation: 86747

To provide a complete solution: it works just by extending ResponseEntityExceptionHandler, as that handles all the spring-mvc errors. And the ones not handled can then be caught using @ExceptionHandler(Exception.class).

@RestControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> exception(Exception ex) {
        MailService.send();
        Logger.logInSpecificWay();
        return ... custom exception 
    }
}

Upvotes: 17

Chaitanya
Chaitanya

Reputation: 131

Create classes for exception handling in this way

@RestControllerAdvice
public class MyExceptionHandler extends BaseExceptionHandler {

}

public class BaseExceptionHandler extends ResponseEntityExceptionHandler {

}

Here ResponseEntityExceptionHandler is provided by spring and override the several exception handler methods provided by it related to the requestMethodNotSupported,missingPathVariable,noHandlerFound,typeMismatch,asyncRequestTimeouts ....... with your own exception messages or error response objects and status codes

and have a method with @ExceptionHandler(Exception.class) in MyExceptionHandler where the thrown exception comes finally if it doesn't have a matching handler.

Upvotes: 0

Naresh Joshi
Naresh Joshi

Reputation: 4547

Well, I was facing the same problem some time back and have tried several ways like extending ResponseEntityExceptionHandler but all them were solving some problems but creating other ones.

Then I have decided to go with a custom solution which was also allowing me to send additional information and I have written below code

@RestControllerAdvice
public class MyExceptionHandler {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @ExceptionHandler(NumberFormatException.class)
    public ResponseEntity<Object> handleNumberFormatException(NumberFormatException ex) {
        return new ResponseEntity<>(getBody(BAD_REQUEST, ex, "Please enter a valid value"), new HttpHeaders(), BAD_REQUEST);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Object> handleIllegalArgumentException(IllegalArgumentException ex) {
        return new ResponseEntity<>(getBody(BAD_REQUEST, ex, ex.getMessage()), new HttpHeaders(), BAD_REQUEST);
    }

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<Object> handleAccessDeniedException(AccessDeniedException ex) {
        return new ResponseEntity<>(getBody(FORBIDDEN, ex, ex.getMessage()), new HttpHeaders(), FORBIDDEN);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> exception(Exception ex) {
        return new ResponseEntity<>(getBody(INTERNAL_SERVER_ERROR, ex, "Something Went Wrong"), new HttpHeaders(), INTERNAL_SERVER_ERROR);
    }

    public Map<String, Object> getBody(HttpStatus status, Exception ex, String message) {

        log.error(message, ex);

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("message", message);
        body.put("timestamp", new Date());
        body.put("status", status.value());
        body.put("error", status.getReasonPhrase());
        body.put("exception", ex.toString());

        Throwable cause = ex.getCause();
        if (cause != null) {
            body.put("exceptionCause", ex.getCause().toString());
        }
        return body;
    }

}

Upvotes: 5

Related Questions