Bogdan
Bogdan

Reputation: 320

Spring Boot - handle exception wrapped with BindException

I am looking for a way to handle custom exception thrown during binding of request parameter to DTO field.

I have a cantroller in Spring Boot application as follows

@GetMapping("/some/url")
public OutputDTO filterEntities(InputDTO inputDTO) {
    return service.getOutput(inputDTO);
}

input DTO has few fields, one of which is of enum type

public class InputDTO {

    private EnumClass enumField;
    private String otherField;

    /**
     * more fields
     */
}

user will hit the URL in ths way

localhost:8081/some/url?enumField=wrongValue&otherField=anyValue

Now if user sends wrong value for enumField, I would like to throw my CustomException with particular message. Process of enum instance creation and throwing of exception is implemented in binder

@InitBinder
public void initEnumClassBinder(final WebDataBinder webdataBinder) {
    webdataBinder.registerCustomEditor(
            EnumClass.class,
            new PropertyEditorSupport() {
                @Override
                public void setAsText(final String text) throws IllegalArgumentException {
                    try {
                        setValue(EnumClass.valueOf(text.toUpperCase()));
                    } catch (Exception exception) {
                        throw new CustomException("Exception while deserializing EnumClass from " + text, exception);
                    }
                }
            }
    );
}

Problem is that when exception is thrown it is impossible to handle it with

@ExceptionHandler(CustomException.class)
public String handleException(CustomException exception) {
    // log exception
    return exception.getMessage();
}

Spring wraps initial exception with BindException. That instance contains my initial error message, but concatenated with other text which is redundant for me. I don't think that parsing and substringing that message is good...

Am I missing something? What is the proper way to get message from initial CustomException here?

Upvotes: 5

Views: 7117

Answers (1)

bogdotro
bogdotro

Reputation: 39

You will not be able to handle exceptions thrown before entering your controller method by using @ExceptionHandler annotated methods. Spring handles these exceptions before entering the controller, by registering DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver handler. This is the case of BindingException, thrown when Spring cannot bind request parameters to match your InputDTO object. What you can do is to register your own handler (create a Component implementing HandlerExceptionResolver and Ordered interfaces), give it the highest priority in handling errors and play with exceptions as needed. You have also to pay attention to BindException as it wrappes your custom exception, CustomException.class

import java.io.IOException;

import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger; import org.slf4j.LoggerFactory; 
import org.springframework.core.Ordered; 
import org.springframework.stereotype.Component; 
import org.springframework.validation.BindException; 
import org.springframework.validation.ObjectError; 
import org.springframework.web.servlet.HandlerExceptionResolver; 
import org.springframework.web.servlet.ModelAndView;


import yourpackage.CustomException;

@Component() 
public class BindingExceptionResolver implements HandlerExceptionResolver, Ordered {
    private static final Logger logger = LoggerFactory.getLogger(BindingExceptionResolver.class);

    public BindingExceptionResolver() {
    }

    private ModelAndView handleException(ObjectError objectError, HttpServletResponse response){
        if (objectError == null) return null;
        try {
            if(objectError.contains(CustomException.class)) {
                CustomException ex = objectError.unwrap(CustomException.class);
                logger.error(ex.getMessage(), ex);
                return handleCustomException(ex, response);
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

    protected ModelAndView handleCustomException(CustomException ex, HttpServletResponse response) throws IOException {
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
        return new ModelAndView();
    }

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            if (ex instanceof org.springframework.validation.BindException) {
                BindException be = (BindException) ex;
                logger.debug("Binding exception in {} :: ({}) :: ({})=({})", be.getObjectName(), be.getBindingResult().getTarget().getClass(), be.getFieldError().getField(), be.getFieldError().getRejectedValue());
                return be.getAllErrors().stream()
                    .filter(o->o.contains(Exception.class))
                    .map(o ->handleException(o, response))
                    .filter(mv ->mv !=null)
                    .findFirst().orElse(null);
            }
        } catch (Exception handlerException) {
            logger.error("Could not handle exception", handlerException); 
        }
        return null;
    }


    @Override
    public int getOrder() {
        return Integer.MIN_VALUE;
    }

}

Hope it helps

Upvotes: 4

Related Questions