Skip to content

Instantly share code, notes, and snippets.

@rponte
Created October 7, 2021 12:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rponte/3715070f5ed2c0888af953706c29ef5e to your computer and use it in GitHub Desktop.
Save rponte/3715070f5ed2c0888af953706c29ef5e to your computer and use it in GitHub Desktop.
Spring Boot: custom and simple exception handler (using the default payload generated by Spring)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
import java.util.Map;
@RestControllerAdvice
public class CustomExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<?> handleDatabaseErrors(Exception ex, WebRequest request) {
logger.error("Catching an unhandled database exception thrown by a controller: " + ex.getLocalizedMessage(), ex);
Map<String, Object> body = Map.of(
"status", 500,
"error", "Internal Server Error",
"path", request.getDescription(false).replace("uri=", ""),
"timestamp", LocalDateTime.now(),
"message", "Ocorreu um erro interno. Por favor contate o administrador."
);
return ResponseEntity
.internalServerError().body(body);
}
}
@rponte
Copy link
Author

rponte commented May 5, 2022

Spring Boot MockMvc does NOT send payload when there's a expcetion of types ResponseStatusException or MethodArgumentNotValidException during the tests. It happens because MockMvc is NOT a real Servlet environment.

Some issues:

Here one of many workarounds to handle this issue:

import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
import org.springframework.boot.web.servlet.error.ErrorController
import org.springframework.http.ResponseEntity
import org.springframework.validation.BindException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import javax.servlet.http.HttpServletRequest

/**
 * This advice is necessary because MockMvc is not a real servlet environment, therefore it does not redirect error
 * responses to [ErrorController], which produces validation response. So we need to fake it in tests.
 * It's not ideal, but at least we can use classic MockMvc tests for testing error response + document it.
 */
@ControllerAdvice
internal class MockMvcValidationConfiguration(private val errorController: BasicErrorController) {

    // add any exceptions/validations/binding problems
    @ExceptionHandler(MethodArgumentNotValidException::class, BindException::class)
    fun defaultErrorHandler(request: HttpServletRequest, ex: Exception): ResponseEntity<*> {
        request.setAttribute("javax.servlet.error.request_uri", request.pathInfo)
        request.setAttribute("javax.servlet.error.status_code", 400)
        return errorController.error(request)
    }
}

And a Java version:

import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpServletRequest;

/**
 * This advice is necessary because MockMvc is not a real servlet environment, therefore it does not redirect error
 * responses to [ErrorController], which produces validation response. So we need to fake it in tests.
 * It's not ideal, but at least we can use classic MockMvc tests for testing error response + document it.
 */
@Profile("test")
@ControllerAdvice
public class KeepErrorMessagesWithMockMvcAdvice {

    private final BasicErrorController errorController;

    public KeepErrorMessagesWithMockMvcAdvice(BasicErrorController errorController) {
        this.errorController = errorController;
    }

    // add any exceptions/validations/binding problems
    @ExceptionHandler({ MethodArgumentNotValidException.class, BindException.class } )
    public ResponseEntity<?> defaultErrorHandler(Exception e, HttpServletRequest request) {
        request.setAttribute("javax.servlet.error.status_code", 400);
        request.setAttribute("javax.servlet.error.request_uri", request.getPathInfo());
        return errorController.error(request);
    }
}

@rponte
Copy link
Author

rponte commented May 5, 2022

Since Spring Boot 2.6.x we need to include server errors explicitly, stacktrace etc.

So, configure your application.properties like this:

server.error.include-message=always
server.error.include-binding-errors=always
server.error.include-stacktrace=on_param
server.error.include-exception=false

Or configure your application.yml like this:

server:
  error:
    include-message: always
    include-binding-errors: always
    include-stacktrace: on_param
    include-exception: false    

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment