15

I have a @ControllerAdvice extending ResponseEntityExceptionHandler as an attempt for me to control standard response for any exception raised with in the API call workflow.

Without the Controller advice. I get HTML based generic response generated by spring with correct response headers. But when I add my @ControllerAdvice, Spring doesn't response with generic error body. The body is empty with correct response headers

@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex,
        HttpHeaders headers, HttpStatus status, WebRequest request) {

        String erroMessage = "Required Parameter: '"+ex.getParameterName()+"' was not available in the request.";
        TrsApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, erroMessage, ex, ApiErrorCode.INVALID_REQUEST);
        return buildResponseEntity(apiError);
}

So, now in case of a required parameter missing in the request, the flow beautifully trigger my overridden implementation and responds with JSON payload describing the error. But, in case of any other exception like HttpMediaTypeNotAcceptableException, spring is responding with empty body.

Before I added my advice, spring was responding with generic error response. I am new to spring boot ecosystem. Need help in understanding if this is an expected behavior of if there is a better approach of achieving centralized error handling.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Gagandeep Singh
  • 151
  • 1
  • 1
  • 4

3 Answers3

10

I guess that I found out a solution for swallowed body when ControllerAdvice class is extending ResponeEntityExceptionHandler. In my case the setup looks like that:

@ControllerAdvice
@Slf4j
class GlobalExceptionHandlers extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
                                      MethodArgumentNotValidException exception,
                                      HttpHeaders headers,
                                      HttpStatus status,
                                      WebRequest request) {
        // logic that creates apiError object (object with status, message, errorCode, etc)
        //...
        return handleExceptionInternal(exception, apiError, headers, status, request);
    }

And this worked like a charm for exceptions of class MethodArgumentNotValidException. But it broke all other exceptions handled by ResponseEntityExceptionHandler, and returned empty response body for them.

But the fix is easy, just override handleExceptionInternal from ResponseEntityExceptionHandler:

@ControllerAdvice
@Slf4j
class GlobalExceptionHandlers extends ResponseEntityExceptionHandler {

    /// ... code from previous snippet

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(
                                      Exception exception, 
                                      Object body, 
                                      HttpHeaders headers, 
                                      HttpStatus status, 
                                      WebRequest request) {
        // for all exceptions that are not overriden, the body is null, so we can
        // just provide new body based on error message and call super method
        var apiError = Objects.isNull(body) 
                ? new ApiError(status, exception.getMessage()) // <-- 
                : body;
        return super.handleExceptionInternal(exception, apiError, headers, status, request);
    }
}
Pawel Kiszka
  • 812
  • 7
  • 11
  • Makes sense, but for me it is not calling any methods from ResponseEntityExceptionHandler at all, something else eats up the body. – user3852017 Feb 22 '22 at 09:19
  • @user3852017 are you calling passing through nginx proxy? check my question here https://stackoverflow.com/questions/72417918/spring-boot-missingservletrequestparameterexception-not-thrown-if-request-forwa – batt May 28 '22 at 19:48
3

This is expected behavior.Look at the source code of the class ResponseEntityExceptionHandler.

@ExceptionHandler({
            org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException.class,
            HttpRequestMethodNotSupportedException.class,
            HttpMediaTypeNotSupportedException.class,
            HttpMediaTypeNotAcceptableException.class,
            MissingPathVariableException.class,
            MissingServletRequestParameterException.class,
            ServletRequestBindingException.class,
            ConversionNotSupportedException.class,
            TypeMismatchException.class,
            HttpMessageNotReadableException.class,
            HttpMessageNotWritableException.class,
            MethodArgumentNotValidException.class,
            MissingServletRequestPartException.class,
            BindException.class,
            NoHandlerFoundException.class,
            AsyncRequestTimeoutException.class
        })
    public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) {

All of these exceptions are handled WITHOUT the response body. A common method is invoked :

//second parameter is body which is null
handleExceptionInternal(ex, null, headers, status, request)

If you need to handle specific exceptions differently, override them, example where I wanted to send a custom response for HttpMessageNotReadableException

 @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
        HttpHeaders headers, HttpStatus status, WebRequest request)
    {
        logger.error("handleHttpMessageNotReadable()", ex);
        ValidationErrors validationErrors = null;
        if (ex.getRootCause() instanceof InvalidFormatException) {
            InvalidFormatException jacksonDataBindInvalidFormatException = (InvalidFormatException) ex.getRootCause();
            validationErrors = new ValidationErrors(jacksonDataBindInvalidFormatException.getOriginalMessage());
        }
        headers.add("X-Validation-Failure", "Request validation failed !");
        return handleExceptionInternal(ex, validationErrors, headers, status, request);
    }
TechFree
  • 2,600
  • 1
  • 17
  • 18
  • When i dont have my advice in place, then spring sends the error stack in response body. I want spring to behave in the same manner except for the methods that I have overridden. For example: I want to provide my own implementation handleMethodArgumentNotValid, but, I need spring to handle handleHttpMediaTypeNotAcceptable – Gagandeep Singh Jun 10 '19 at 17:01
  • What happens when you dont extend ResponseEntityExceptionHandler ? Can you check that for a custom exception and for a Spring MVC exception handleHttpMediaTypeNotAcceptable – TechFree Jun 11 '19 at 01:57
  • So when I don't extend from ResponseEntityExceptionHandler then spring starts handling with proper response body for handleHttpMediaTypeNotAcceptable – Gagandeep Singh Jun 11 '19 at 13:48
  • `code`

    Whitelabel Error Page

    This application has no explicit mapping for /error, so you are seeing this as a fallback.

    Tue Jun 11 09:44:35 EDT 2019
    There was an unexpected error (type=Not Acceptable, status=406).
    Could not find acceptable representation
    org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation `code`
    – Gagandeep Singh Jun 11 '19 at 13:50
  • OK, then dont extend from responseEntity.... and just add your custom @ExceptionHandler... @ExceptionHandler(RuntimeException.class) public ResponseEntity handleRuntimeException(RuntimeException ex, WebRequest request) – TechFree Jun 11 '19 at 13:50
  • If I do that then, I wont be able to handle handleMissingServletRequestParameter. I want to respond to this error following my standard json response format – Gagandeep Singh Jun 11 '19 at 19:48
2

You need to define generic exception structure once you use @ControllerAdvice.

@ResponseBody
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse generationExceptionHandler(Exception e){
    log.info("Responding INTERNAL SERVER ERROR Exception");
    return new ErrorResponse(ServiceException.getSystemError());
}
Sushil Behera
  • 817
  • 6
  • 20
  • I have the following: @ResponseBody @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) protected ResponseEntity handleDefaultException(Exception ex) { logger.error("", ex); ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage(), ex); } return buildResponseEntity(apiError); } So, now if I create a request without keeping a mandatory field, the response to my client is coming with empty body and 404 header. Even my breakpoints inside the methods are not getting triggered. – Gagandeep Singh Jun 10 '19 at 15:01
  • Have you extended you class with ResponseEntityExceptionHandler ?? – Sushil Behera Jun 10 '19 at 15:30
  • and override this method protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) – Sushil Behera Jun 10 '19 at 15:31
  • I have extended my class as I have mentioned in the description. My Issue is that if i override the handleMethodArgumentNotValid then it takes my implementation. But, if I don't then it is not taking the default implementation of sending spring error stack as the response. – Gagandeep Singh Jun 10 '19 at 16:56