Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling exception in Spring Boot REST thrown from custom converter

I'm new to Spring Boot but after few hours of reading posts and blogs about exception handlig in Spring Boot REST where nobody wrote anything about handling such exception thrown from custom Converter, I decided to write here.

I develop small REST app based on Spring Boot simply generated from IntelliJ. Exemplary method looks like this

@RestController
@RequestMapping("/resources")
public class CVResourceService {

    private final TechnologyRepository technologyRepository;
    private final ProjectRepository projectRepository;

    @Autowired
    public CVResourceService(TechnologyRepository technologyRepository,     ProjectRepository projectRepository) {
        this.technologyRepository = technologyRepository;
        this.projectRepository = projectRepository;
    }

    @RequestMapping(value = "/users/{guid}/projects/langs/{lang}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Collection getUserProjects(@PathVariable("guid") GUID userGUID,         @PathVariable("lang") Language language) {
        return  ProjectDTOAssembler.toDTOs(projectRepository.findOne(userGUID, language));
    }
}

Because both guid and lang are String and I wanted this pieces of information were strong typed from same begining, I created simply converters for GUID and Language types and registered it in Application class:

public final class GUIDConverter implements Converter{

    @Override
    public GUID convert(String source) {
        return GUID.fromString(source);
    }
}

public class LanguageConverter implements Converter{

    @Override
    public Language convert(String source) {
        Language language = Language.of(source);
        if (language == null) { throw new WrongLanguagePathVariableException(); }

        return language;
    }
}

GUID throws exception from method factory,

...
public static GUID fromString(String string) {
    String[] components = string.split("-");

    if (components.length != 5)
        throw new IllegalArgumentException("Invalid GUID string: " + string);

    return new GUID(string);
}
...

Language return null so then I throw custom exception from converter. Registering in Application:

@SpringBootApplication
public class Application extends WebMvcConfigurerAdapter {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new GUIDConverter());
        registry.addConverter(new LanguageConverter());
    }

    public static void main(String[] args) {
       SpringApplication.run(Application.class, args);
   }
}

Using all kind of handling exception with @ResponseStatus, @ControllerAdvice and @ExpectationHandler I couldn't catch converters' exceptions in Controller to rewrite (or better map) "status", "error", "exception" and "message" of json error response original field to my values. Probably because the exceptions are thrown before the call of my REST method. I tried also solution with ResponseEntityExceptionHandler but it didn't work at all.

For the request http://localhost:8080/resources/users/620e643f-406f-4c69-3f4c-3f2c303f3f3f/projects/langs/end where correct language is en, response with exception is:

{
    "timestamp": 1458812172976,
    "status": 400,
    "error": "Bad Request",
    "exception":   "org.springframework.web.method.annotation.MethodArgumentTypeMismatchException",
    "message": "Failed to convert value of type [java.lang.String] to required type [com.cybercom.cvdataapi.domain.Language]; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.PathVariable com.cybercom.cvdataapi.domain.Language] for value 'end'; nested exception is com.cybercom.cvdataapi.interfaces.rest.converter.WrongLanguagePathVariableException",
    "path": "/resources/users/620e643f-406f-4c69-3f4c-3f2c303f3f3f/projects/langs/end"

}

where custom exception is only at the last position in message field but of course should be exchange with my custom message.And custom excetion should be in exception field where is now Spring exception. it's the goal of course, bu no idea how to achieve it in this context.

Please solve my problem with catching exceptions thrown from converters and mapping them the way as it can be done with @ControllerAdvice and exceptions thrown from Controllers. Thx in advance.

like image 810
Marcin Godlewski Avatar asked Mar 23 '16 22:03

Marcin Godlewski


2 Answers

You are right - @ControllerAdvice etc only catches exceptions originating inside controllers methods - and to be honest, I found it takes some thought to get your head around understanding all the error handling and putting together a consistent error handling approach (without having to duplicate error handling in different places).

Because in my applications I inevitably need to catch errors like these (or custom 404s etc) that are outside the controller I just go for application wide error mapping - I assume you are running the application as a JAR given you have defined the main() method in your application.

My preference is to have a specific error configuration file, for readability, but you can define this as just a normal bean in your config if you want:

@Configuration
class ErrorConfiguration implements EmbeddedServletContainerCustomizer {

   /**
     * Set error pages for specific error response codes
     */
   @Override public void customize( ConfigurableEmbeddedServletContainer container ) {
       container.addErrorPages( new ErrorPage( HttpStatus.NOT_FOUND, "/errors/404" ) )
   }

}

You can map error pages based on specific Exception types as well as Http response codes, so its pretty flexible - but what I normally do is define custom exceptions and attach Http response codes to the exceptions - that way I can map the HttpStatus codes in my error config, and then if anywhere in the code I want to, for exaple, 404 a request I can just throw new PageNotFoundException() (but thats personal preference).

@ResponseStatus(value = HttpStatus.NOT_FOUND)
class PageNotFoundException extends Exception { }

The mapping path (in the above example "/errors/404") maps to a normal controller, which is nice as it lets you do any error logging/processing/emailing etc that you might want to do on a given error - the downside of this is that as it is a standard controller handling your errors, you potentially have an exposed endpoint like /errors/404 - which isn't ideal (there are a few options - obscuring the URL so less likely to be discovered, or using something like apache to prevent direct access to those endpoints etc)

I briefly wrote about it here - including details as to how it works when hosting your app as a WAR in a traditional tomcat server

like image 99
rhinds Avatar answered Sep 17 '22 15:09

rhinds


When your converter throws an exception the actual exception that would be picked up by a @ControllerAdvice class (or other type of exception handler) is a MethodArgumentTypeMismatchException.

If you configure your converters to pick up a MethodArgumentTypeMismatchException instead then it should work.

For Example:

@ControllerAdvice
public class ExceptionConfiguration extends ResponseEntityExceptionHandler {

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResponseEntity<String> handleConverterErrors(MethodArgumentTypeMismatchException exception) {
        Throwable cause = exception.getCause() // First cause is a ConversionException
                                   .getCause(); // Second Cause is your custom exception or some other exception e.g. NullPointerException
        if(cause.getClass() == UnauthorizedException.class) {
            return ResponseEntity.status(401).body("Your session has expired");
        }
        return ResponseEntity.badRequest().body("Bad Request: [" + cause.getMessage() + "]");
    }
}

In the case above if my custom exception was thrown the response would look like:

Status: 401

Your session has expired

Otherwise for other exceptions it would be

Status: 400

Bad Request: [The exception message]

You can customise the response as you like and return JSON if you prefer.

To get the path as well you can inject a HttpServletRequest into your error handler method

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<String> 
handleConverterErrors(HttpServletRequest req, MethodArgumentTypeMismatchException exception) {
    String path = req.getPathInfo()
    //...
}
like image 38
Eduardo Avatar answered Sep 16 '22 15:09

Eduardo