Spring intercept exceptions in one class - java

I am using Spring Boot for my REST service. Now I want to implement a general logging at a central place. Here is my structure:
REST
#GetMapping(value="/rest/test/{userId}")
public User getUserById(#PathVariable String userId) {
return userService.findById(userId);
}
UserService
public User findById(#NotNull String userId) {
if(noUserFound) {
throw new InvalidArgumentException();
}
}
My goal is to have a central class that intercepts all exceptions (also BeanValidation exceptions) and get the following information:
The API endpoint that was called
All parameters that were passed
The exception and its message
The username/id that sent the request (I am using Keycloak with OpenAuth)
Is there a possibility to do that, maybe without annotating every method with #ExceptionHandler?

Well, you can use #ControllerAdvice to achieve this goal.
Here is a sample code:
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
#ControllerAdvice
public class ErrorHandler extends ResponseEntityExceptionHandler {
// Build-in exceptions, this one is for Validation errors
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request
) {
// info about endpoint
var path = request.getDescription(false);
// request params map
var params = request.getParameterMap();
// authenticated principal
var user = request.getUserPrincipal();
log.debug("path: {}, params: {}, user: {}", path, params.keySet(), user.getName());
return super.handleMethodArgumentNotValid(ex, headers, status, request);
}
// Custom exception
#ExceptionHandler(NoAccessException.class)
public ResponseEntity<Error> noAccessException(NoAccessException ex, WebRequest request) {
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(Error.builder().message(ex.getMessage()).build());
}
}

You can use #ControllerAdvice. Example code from https://mkyong.com/spring-boot/spring-rest-error-handling-example/
#ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler
{
//...
// #Validate For Validating Path Variables and Request Parameters
#ExceptionHandler(ConstraintViolationException.class)
public void constraintViolationException(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value());
}
// error handle for #Valid
#Override
protected ResponseEntity<Object>
handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", new Date());
body.put("status", status.value());
//Get all fields errors
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(x -> x.getDefaultMessage())
.collect(Collectors.toList());
body.put("errors", errors);
return new ResponseEntity<>(body, headers, status);
}}

Related

Spring ControllerAdvice does not return response body?

I have the following ControllerAdvice, that handles JsonParseException (I use Spring and Jackson)
#ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(JsonParseException.class)
public ResponseEntity<Object> handleInvalidJson(JsonParseException ex, WebRequest request){
Map<String,Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("message","Invalid Json");
return new ResponseEntity(body, HttpStatus.BAD_REQUEST);
}
}
Fore some reason, It doesn't work when I send a bad json request to the server, only returns 400. When I change the HttpStatus, it still returns 400 so it seems like the advice doesn't really run.
ResponseEntityExceptionHandler already implements a lot of different ExceptionHandlers. HttpMessageNotReadableException is one of them:
else if (ex instanceof HttpMessageNotReadableException) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, headers, status, request);
}
Simply remove the inheritance:
#ControllerAdvice
public class TestExceptionHandler {
#ExceptionHandler(JsonParseException.class)
public ResponseEntity<Map<String,Object>> handleInvalidJson(JsonParseException ex, WebRequest request){
Map<String,Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("message","Invalid Json");
return new ResponseEntity<>(body, HttpStatus.I_AM_A_TEAPOT);
}
}

How to return the ConstraintViolationException messages in the response body?

I am developing a Spring Boot based REST API. I am validating the input entities using custom ConstraintValidator annotations. My problem is that I cannot return the ConstraintViolationException messages in the response. My exception handler does not catch the exceptions (maybe because they're wrapped in another types of exceptions).
Can I please get some advice on how to handle the situation?
I've searched all over the Internet but I couldn't find a fitting solution for me and I've also wasted some hours doing so.
Example annotation:
#Documented
#Retention(RUNTIME)
#Target({FIELD, PARAMETER})
#Constraint(validatedBy = BirthDateValidator.class)
public #interface ValidBirthDate {
String message() default "The birth date is not valid.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator class:
public class BirthDateValidator extends FieldValidationBase implements ConstraintValidator<ValidBirthDate, LocalDate> {
private static final Logger LOGGER = LoggerFactory.getLogger(BirthDateValidator.class);
#Override
public void initialize(ValidBirthDate constraintAnnotation) {
}
#Override
public boolean isValid(LocalDate birthDate, ConstraintValidatorContext constraintValidatorContext) {
constraintValidatorContext.disableDefaultConstraintViolation();
LOGGER.info("Starting the validation process for birth date {}.", birthDate);
if(birthDate == null) {
constraintValidatorContext.buildConstraintViolationWithTemplate("The birth date is null.")
.addConstraintViolation();
return false;
}
//other validations
return true;
}
Model class:
public class Manager extends BaseUser {
//other fields
#Valid
#ValidBirthDate
private LocalDate birthDate;
//setters & getters
Exception handler:
#ExceptionHandler(value = ConstraintViolationException.class)
public ResponseEntity handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
List<String> errors = new ArrayList<>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(violation.getRootBeanClass().getName() + ": " + violation.getMessage());
}
Error response = new Error(errors);
return new ResponseEntity<Object>(response, new HttpHeaders(), BAD_REQUEST);
}
The controller:
#RestController
#RequestMapping(value = "/register", consumes = "application/json", produces = "application/json")
public class RegistrationController {
#Autowired
private RegistrationService registrationService;
#PostMapping(value = "/manager")
public ResponseEntity registerManager(#RequestBody #Valid Manager manager) {
registrationService.executeSelfUserRegistration(manager);
return new ResponseEntity<>(new Message("User " + manager.getEmailAddress() + " registered successfully!"), CREATED);
}
}
I get the 400 response code, but I am not seeing any response body containing the violated constraint messages.
After some more debugging, I found out that all constraint violations were wrapped into a MethodArgumentNotValidException (because of the #Valid annotations) - I had to dig a bit inside that exception to get my information.
I've overriden the handleMethodArgumentNotValid() method from ResponseEntityExceptionHandler and this is how I got it to work:
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
List<String> errorMessages = new ArrayList<>();
BindingResult bindingResult = ex.getBindingResult();
List<ObjectError> errors = bindingResult.getAllErrors();
for(ObjectError error : errors) {
String message = error.getDefaultMessage();
errorMessages.add(message);
}
return new ResponseEntity<>(new Error(errorMessages), new HttpHeaders(), BAD_REQUEST);
}
Maybe this helps someone.
When the target argument fails to pass the validation, Spring Boot throws a MethodArgumentNotValidException exception. I have extracted the error message from bindingResult of this exception as shown below:
#RestControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
//to extract the default error message from a diagnostic
// information about the errors held in MethodArgumentNotValidException
Exception exception = new Exception(ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return this.createResponseEntity(HttpStatus.BAD_REQUEST, exception, request);
}
private ResponseEntity<Object> createResponseEntity(
HttpStatus httpStatus, Exception ex, WebRequest request) {
ErrorResponse errorResponse = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(httpStatus.value())
.error(httpStatus.getReasonPhrase())
.message(ex.getMessage())
.path(request.getDescription(true))
.build();
return handleExceptionInternal(ex, errorResponse,
new HttpHeaders(), httpStatus, request);
}
}
ErrorResponse class:
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
}
The response will be 400 with body in JSON format as shown below:
{
"timestamp": "2021-01-20T10:30:15.011468",
"status": 400,
"error": "Bad Request",
"message": "Due date should not be greater than or equal to Repeat Until Date.",
"path": "uri=/api/tasks;client=172.18.0.5;user=109634489423695603526"
}
I hope this helps. If you need a detailed explanation on class-level constraint, have a look at this video.

Spring Boot how to return my own validation constraint error messages

I need to have my own error response body when something goes wrong with my request and I am trying to use the #NotEmpty constraint message attribute to return the error message,
This is my class that returns the error message using the body that I need:
package c.m.nanicolina.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
#ControllerAdvice
public class CustomResponseEntityExceptionHandler {
#ExceptionHandler(value = {MissingServletRequestParameterException.class})
public ResponseEntity<ApiError> handleConflict(MissingServletRequestParameterException ex, WebRequest request) {
ApiError apiError = new ApiError(ex.getMessage(), ex.getMessage(), 1000);
return new ResponseEntity<ApiError>(apiError, null, HttpStatus.BAD_REQUEST);
}
}
With this CustomResponseEntityExceptionHandler I can return my own response body in case of validation errors.
What I am trying now is to get the message from the validation constraints.
This is my controller with the NotEmpty constraint:
package c.m.nanicolina.controllers;
import c.m.nanicolina.models.Product;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotEmpty;
#RestController
public class MinimumStockController {
#RequestMapping(value = "/minimumstock")
public Product product(
#RequestParam(value = "product.sku") #NotEmpty(message = "Product.sku cannot be empty") String sku,
#RequestParam(value = "stock.branch.id") String branchID) {
return null;
}
}
In my exception, I can't find a way to get that message Product.sku cannot be empty and show it in my error response.
I have also checked the class MissingServletRequestParameterException and there is the method getMessage which is returning the default message.
Yes it is doable & spring very well supports it. You are just missing some configuration to enable it in spring.
Use Spring#Validated annotation to enable spring to validate controller
Handle ConstraintViolationException in your ControllerAdvice to catch all failed validation messages.
Mark required=false in #RequestParam, so it will not throw MissingServletRequestParameterException and rather move to next step of constraint validation.
#ControllerAdvice
public class CustomResponseEntityExceptionHandler {
#ExceptionHandler
public ResponseEntity<ApiError> handle(ConstraintViolationException exception) {
//you will get all javax failed validation, can be more than one
//so you can return the set of error messages or just the first message
String errorMessage = new ArrayList<>(exception.getConstraintViolations()).get(0).getMessage();
ApiError apiError = new ApiError(errorMessage, errorMessage, 1000);
return new ResponseEntity<ApiError>(apiError, null, HttpStatus.BAD_REQUEST);
}
}
#RestController
#Validated
public class MinimumStockController {
#RequestMapping(value = "/minimumstock")
public Product product(
#RequestParam(value = "product.sku", required=false) #NotEmpty(message = "Product.sku cannot be empty") String sku,
#RequestParam(value = "stock.branch.id", required=false) String branchID) {
return null;
}
}
NOTE: MissingServletRequestParameterException won't have access to javax validation messages, as it is thrown before constraint validation occurs in the request lifecycle.
Yes it is possible. Do this:
#ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ArgumentsErrorResponseDTO> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
ServiceException exception = ServiceException.wrap(ex, ErrorCode.FIELD_VALIDATION);
BindingResult results = ex.getBindingResult();
for (FieldError e: results.getFieldErrors()) {
exception.addLog(e.getDefaultMessage(), e.getField());
}
// log details in log
log.error("Invalid arguments exception: {}", exception.logWithDetails(), exception);
return ResponseEntity.status(exception.getErrorCode().getHttpStatus())
.body(ArgumentsErrorResponseDTO.builder()
.code(exception.getErrorCode().getCode())
.message(exception.getMessage())
.details(exception.getProperties())
.build());
}
If this can help, I found the solution for this issue here: https://howtodoinjava.com/spring-boot2/spring-rest-request-validation/
You have to add this method to your CustomResponseEntityExceptionHandler class:
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
List<String> details = new ArrayList<>();
for(ObjectError error : ex.getBindingResult().getAllErrors()) {
details.add(error.getDefaultMessage());
}
ErrorMessage error = new ErrorMessage(new Date(), details.toString());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
You should put this on your handler.
#ControllerAdvice
public class CustomResponseEntityExceptionHandler {
#ExceptionHandler(value = { MissingServletRequestParameterException.class })
public ResponseEntity<ApiError> handleConflict(MissingServletRequestParameterException ex, WebRequest request) {
String message = ex.getParameterName() + " cannot be empty";
ApiError apiError = new ApiError(ex.getMessage(), message, 1000);
return new ResponseEntity < ApiError > (apiError, null, HttpStatus.BAD_REQUEST);
}
}
UPDATE
I don't know how you can get a default message, but as a workaround, you could do the validation on your controller and throw an custom exception if the parameter is empty, then handle in your CustomResponseEntityExceptionHandler.
Something like the following:
Set required=false
#RequestMapping(value = "/minimumstock")
public Product product(#RequestParam(required = false) String sku, #RequestParam(value = "stock.branch.id") String branchID) {
if (StringUtils.isEmpty(sku))
throw YourException("Product.sku cannot be empty");
return null;
}
Use #ExceptionHandler(MethodArgumentNotValidException.class)
#ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> validationHandler(MethodArgumentNotValidException exception) {
HashMap<String, Object> resObj = new HashMap<String, Object>();
String errorMsg = "validation is failed!";
if (exception.getErrorCount() > 0) {
List <String> errorDetails = new ArrayList<>();
for (ObjectError error : exception.getBindingResult().getAllErrors()) {
errorDetails.add(error.getDefaultMessage());
}
if (errorDetails.size() > 0) errorMsg = errorDetails.get(0);
}
resObj.put("status", GlobalConst.BAD_REQUEST_CODE);
resObj.put("message", errorMsg);
return new ResponseEntity<>(resObj, HttpStatus.OK);
}

Add Query Parameter to Every REST Request using Spring RestTemplate

Is there a way to add a query parameter to every HTTP request performed by RestTemplate in Spring?
The Atlassian API uses the query parameter os_authType to dictate the authentication method so I'd like to append ?os_authtype=basic to every request without specifying it all over my code.
Code
#Service
public class MyService {
private RestTemplate restTemplate;
#Autowired
public MyService(RestTemplateBuilder restTemplateBuilder,
#Value("${api.username}") final String username, #Value("${api.password}") final String password, #Value("${api.url}") final String url ) {
restTemplate = restTemplateBuilder
.basicAuthorization(username, password)
.rootUri(url)
.build();
}
public ResponseEntity<String> getApplicationData() {
ResponseEntity<String> response
= restTemplate.getForEntity("/demo?os_authType=basic", String.class);
return response;
}
}
You can write custom RequestInterceptor that implements ClientHttpRequestInterceptor
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
public class AtlassianAuthInterceptor implements ClientHttpRequestInterceptor {
#Override
public ClientHttpResponse intercept(
HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
// logic to check if request has query parameter else add it
return execution.execute(request, body);
}
}
Now we need to configure our RestTemplate to use it
import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
#Configuration
public class MyAppConfig {
#Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());
restTemplate.setInterceptors(Collections.singletonList(new AtlassianAuthInterceptor()));
return restTemplate;
}
}
For the ones interested in logic to add a query parameter, as HttpRequest is immutable a wrapper class is needed.
class RequestWrapper {
private final HttpRequest original;
private final URI newUriWithParam;
...
public HttpMethod getMethod() { return this.original.method }
public URI getURI() { return newUriWithParam }
}
Then in your ClientHttpRequestInterceptor you can do something like
public ClientHttpResponse intercept(
request: HttpRequest,
body: ByteArray,
execution: ClientHttpRequestExecution
) {
URI uri = UriComponentsBuilder.fromUri(request.uri).queryParam("new-param", "param value").build().toUri();
return execution.execute(RequestWrapper(request, uri), body);
}
Update
Since spring 3.1 wrapper class org.springframework.http.client.support.HttpRequestWrapper is available in spring-web

customizing error response message when #requestparam is missing

I have already looked at a bunch of existing pages on stackoverflow for this but none of these helped:
How to customize #RequestParam error 400 response in Spring MVC
How to inform callers when required rquest parameter is missing?
My problem is extremely similar:
My application is really a REST api, returning json/xml data rather that html (ie I don't use jsps, but marshalling to transform java beans to json/xml)
When I query the URL with a required parameter, I get a 400 status with nothing in the payload, no text, nada.
I would like to return a json/xml payload using my ErrorResponse object that I use in other places.
I am using Spring 3.2.5
I have a controller which maps a URL to a method with a required
parameter(candidateBillDay)
#Controller
public class AccountController extends WsController {
private static final String JSP_CANDIDATE_PDD = "candidatepdd";
#RequestMapping( value="/account/{externalId}/{externalIdType}/candidatepdd"
, method = RequestMethod.GET)
public String getCandidatePaymentDueDateInfo( ModelMap model
, #PathVariable String externalId
, #PathVariable Integer externalIdType
, #RequestParam Integer candidateBillDay
, #RequestParam(required=false) Boolean includeCurrent ){
...
model.addAttribute( CandidatePaymentDueDateResponse.ROOT_ELEMENT, ...));
return JSP_CANDIDATE_PDD;
}
}
I have an exception handler that catches all types of exceptions, has some logic to do some specific bits for some types (instanceof):
#ControllerAdvice
public class BWSExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(BWSExceptionHandler.class);
#ExceptionHandler(value = { Exception.class } )
public ResponseEntity<Object> handleOtherExceptions(final Exception ex, final WebRequest req) {
LOG.error("Uncaught Exception: ", ex);
ErrorResponse resp = null;
...
if( ex instanceof MissingServletRequestParameterException ){
MissingServletRequestParameterException e = (MissingServletRequestParameterException)ex;
resp = new ErrorResponse( Validatable.ERR_CODE_FIELD_NOT_POPULATED
, String.format( Validatable.MSG_FIELD_IS_REQUIRED
, e.getParameterName()
)
);
httpStatusCode = HttpStatus.BAD_REQUEST;
}
if(resp==null){
resp = new ErrorResponse(new ErrorElement("unknown_error", ex.getMessage()));
}
return handleExceptionInternal(ex, resp, new HttpHeaders(), httpStatusCode, req);
}
}
So this doesn't do anything when a parameter is missing. When I get an actual exception (ie account doesn't exist) then it does catch the exception and works as excepted. This leads me to think that no MissingServletRequestParameterException exception is thrown, which according to the doc, blogs and stackoverflow pages I've read should be thrown...
I have also tried implementing a class that extends DefaultHandlerExceptionResolver and override the handleMissingServletRequestParameter method with not much success ( following this blog: http://alexcuesta.wordpress.com/2011/05/11/error-handling-and-http-status-codes-with-spring-mvc/ )
Any idea of what I am doing wrong or what other option should I explore?
Try overriding handleMissingServletRequestParameter method in the BWSExceptionHandler class.
#ControllerAdvice
public class BWSExceptionHandler extends ResponseEntityExceptionHandler {
...
#Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
// MissingServletRequestParameterException handling code goes here.
}
...
#ExceptionHandler(value = { Exception.class } )
public ResponseEntity<Object> handleOtherExceptions(final Exception ex,
final WebRequest req) {
...
}
}
Hope this helps.

Categories