Spring: RESTful controllers and error handling

07 Dec 2011

Restful MVC controllers

Our current project is based on Spring MVC with mix of other technologies. Like in many projects, we handle errors with a global error handler that redirects to a help page. It works great: regardless of where the exception is thrown, if no one catches it the user gets taken to a page where he can try resuming or contact support.

warning

The problem is client/server interactions… what about our RESTful controllers? The browser sends JSON, and it definitely expects JSON back! Let's have a look at a request that failed on the server side:

HTTP 200 header

HTTP 200 payload

In case you're wondering: yes, that's the HTML content of the error page, sent straight back to our unsuspecting Javascript code! gasp But that's not the only problem: the HTTP status code also tells the client everything went fine. What we really want is a proper way to let the web client know what happened:

Spring annotations

Reading through the docs, there's a few ways to handle errors in Spring MVC. Our friend here will be annotations-based resolvers, which allow you to easily control error handling based the location and type of the exception. They are enabled by default, but not active if you registered other custom resolvers. If that's the case, the next few lines should help:

@Component
public class AnnotatedExceptionResolver extends AnnotationMethodHandlerExceptionResolver
{
    public AnnotatedExceptionResolver() {
        setOrder(HIGHEST_PRECEDENCE);
    }
}

So as a first step let's make all controllers extend the same base class. There are other approaches but that's a good place to start.

@Controller
@RequestMapping (value = "/services")
public class MyCoolController extends BasicController { ... }

This base class can now define how all controllers catch exceptions on any routes, with the @ExceptionHandler annotation. Interestingly it doesn't take more than 5 lines of code to achieve what we want!

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

public class BasicController
{
    @ExceptionHandler (Exception.class)
    @ResponseStatus (HttpStatus.INTERNAL_SERVER_ERROR)
    public ModelAndView handleAllExceptions(Exception ex) {
        return new JsonError(ex.getMessage()).asModelAndView();
    }
}

The code here catches all exceptions, but can be overloaded to catch any specific type, and return any HTTP status code. Note that derived controllers can also overload handleAllExceptions with more specific exceptions, in which case the most specific version will always be called.

So what about that JsonError class? That's a simple class that defines what our generic JSON error looks like. In our case it's a simple message, but of course it can get more complex: we could return many different types of responses, or even serialiase objects instead of using a map.

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJacksonJsonView;
import com.google.common.collect.ImmutableMap;

public class JsonError
{
    private final String message;

    public JsonError(String message) {
        this.message = message;
    }

    public ModelAndView asModelAndView() {
        MappingJacksonJsonView jsonView = new MappingJacksonJsonView();
        return new ModelAndView(jsonView, ImmutableMap.of("error", message));
    }
}

The result

That's it! Any exception thrown during a call to a RESTful controller will now give the following result:

HTTP 500 header

HTTP 500 payload

We can now safely inspect the response when it comes back, optionally looking at xhr.status and xhr.responseText to know more about the problem.

$.ajax({
    // ...
    success: function (response) { ... },
    error: function (xhr, ajaxOptions, thrownError) {
        // we should let the user know
    }    
});

Comments