REST API exception handling using JAX RS in JEE 6

It’s considered a good practice to use HTTP Status Codes in RESTful APIs for sharing the outcome of an API call.

There are plenty of HTTP Status codes to make use of. Unfortunately complex cases lead developers to returning custom errors rather than sticking to the HTTP standards. But there is a way to stick to the standard and still make use of custom errors as per the application needs. Let’s see an approach using JAXRS Exception mapping providers.

  • For simple requests the service may indeed return a single HTTP response code which makes sense.
  • But for complex requests which fail a more detailed error list (business specific) may be desired.

Let’s start building a JAXRS RESTful service to handle the simple case first and then see what we can do to handle the complex case later using the Exception mapping providers.

The code is available here https://github.com/prashantpro/jaxrs-movie-service

Scenario

Consider a movie library service which returns the list of movies released on a given year.

Resource

Method: GET

URL: /movie/{year}

Parameters: Year

Response HTTP Status Code Description
Success 200 – Ok Movie list is returned
Input Invalid 400 – Bad Request The input year maybe invalid
Failure 500 – Internal Error Uncaught exception occurred such as RuntimeException

Setup a Web project

Create a dynamic web project in eclipse and name it jaxrs-movie-service. I have used a JEE 6 server i.e. JBoss 7.

Note: You can use maven and create a maven-archetype-webapp project as well. We won’t need any jars in our lib as JAXRS is part of JEE 6.

Deployment descriptor updates

Update web.xml to match the below

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	id="WebApp_ID" version="3.0">
	<display-name>jaxrs-movie-service</display-name>
	
	<servlet-mapping>
		<servlet-name>javax.ws.rs.core.Application</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
</web-app>

Create an empty beans.xml under WEB-INF folder. (This will enable CDI capabilities which can be made use of if desired).

The source code

Let’s define a POJO which will hold the movie information.

package org.prashantpro.jaxrs.movie;

/**
 * @author Prashant Padmanabhan <https://javaspecialist.wordpress.com>
 *
 */
public class Movie {

	private int year;
	private String title;
	private String genre;

	public Movie(int year, String title, String genre) {
		this.year = year;
		this.title = title;
		this.genre = genre;
	}
	//Getters and setters left out for brevity.
	...
}

Next, create the REST service which will expose the movie libraries movie list. Here we aren’t using any checked business exception handlers. This is a simple service which returns the HTTP Status code based on conditional constructs.

package org.prashantpro.jaxrs.movie;

import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

/**
 * @author Prashant Padmanabhan <https://javaspecialist.wordpress.com>
 *
 */
@Path("/movie")
@Produces(MediaType.APPLICATION_JSON)
public class MovieLibrary {
	//Store the in memory movie list as our repository.
	static final List MOVIE_LIST = new ArrayList();

	//Build a dummy list of movies to work with.
	static {
		MOVIE_LIST.add(new Movie(1971,"Dirty Harry","Action"));
		MOVIE_LIST.add(new Movie(2008,"Gran Torino","Drama"));
		MOVIE_LIST.add(new Movie(2012,"Argo","Drama"));
	}

	@GET
	@Path("/{year}")
	public Response getMovies(@PathParam("year") int year) {
		if(year < 1880 || year > 9999) {
			//Invalid input for year so return HTTP Status 400
			return Response.status(Response.Status.BAD_REQUEST).build();
		}
		List list = getMoviesByYear(year);
		return Response.status(Response.Status.OK).entity(list).build();
	}

	private List getMoviesByYear(int targetYear) {
		List found = new ArrayList();
		for(Movie movie : MOVIE_LIST) {
			if(movie.getYear() == targetYear)
				found.add(movie);
		}
		return found;
	}

}

Once the above code is deployed as a WAR file in a JEE 6 server such as JBoss 7, we can then hit the URL:

http://localhost:8080/jaxrs-movie-service/movie/1971

Where;

jaxrs-movie-service – Is the context name of our application

/movie – Is the REST resource identified by @Path(“/movie”)

1971 – Is the input which maps to the getMovies method via @Path(“/{year}”)

The above should result in the below response:

Response headers:

  • Status Code: 200 OK
  • Content-Type: application/json

Response body:

[{"year":1971,"title":"Dirty Harry","genre":"Action"}]

This was simple to begin with, but in real world we would have some facade or service layer which would do the processing. The facade may throw business exceptions which would then result in passing a different response code with an appropriate message.

The complex case discussed below explains the same.

Complex case

Consider the movie library service would also require to list out movies based on a genre in the given order of ASC or DESC.

For simplicity let’s say we have only two genres “Action” and “Drama“.

In case of any problem the detailed message must be returned by the REST Service. This means we need to return some error messages along with the right HTTP Status code.

We can start throwing business exceptions but these won’t get translated to an appropriate HTTP Response code.

Here, we make use of Exception Mapping providers. These map a checked or runtime exception to an instance of Response. An exception mapping provider implements the ExceptionMapper<T> interface and is annotated with @Provider.

Resource

Method: GET

URL: /movie/list

Parameters: order, genre

Response HTTP Status Code Description
Success 200 – Ok Movie list is returned
Input Invalid 400 – Bad Request Service must return the details of what went wrong.Example genre specified was wrong.Sort order related issue.

Let’s create a application exception which would contain the error details.

package org.prashantpro.jaxrs.movie;

import java.util.List;

/**
 * @author Prashant Padmanabhan <https://javaspecialist.wordpress.com>
 *
 */
public class BusinessException extends Exception {

	private static final long serialVersionUID = 1L;

	private List messages;

	public BusinessException(List messages) {
		super();
		this.messages = messages;
	}

	public List getMessages() {
		return messages;
	}
}

Now, the Exception Mapper which will map our Business Exception to the correct Response code.

package org.prashantpro.jaxrs.movie;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

/**
 * @author Prashant Padmanabhan <https://javaspecialist.wordpress.com>
 *
 */
@Provider
public class ExceptionHttpStatusResolver implements
		ExceptionMapper {

	@Override
	public Response toResponse(BusinessException exception) {
		Response.Status httpStatus = Response.Status.INTERNAL_SERVER_ERROR;

		if (exception instanceof BusinessException)
			httpStatus = Response.Status.BAD_REQUEST;

		return Response.status(httpStatus).entity(exception.getMessages())
				.build();
	}
}

The MovieLibrary.java gets a new method as shown below:

	@GET
	@Path("/list")
	public Response getMoviesByGenre(@QueryParam("order") String order,@QueryParam("genre") String genre) throws BusinessException {
		List errorMessages = new ArrayList();

		if(order == null || order.length() == 0) {
			errorMessages.add("order is required");
		}

		if(genre == null || genre.length() == 0) {
			errorMessages.add("genre is required");
		}

		if(!"ASC".equals(order) && !"DESC".equals(order)) {
			errorMessages.add("order of either ASC or DESC must be specified");
		}

		if(!"Action".equals(genre) &&  !"Drama".equals(genre)) {
			errorMessages.add("genre of either Action or Drama must be specified");
		}
		if(!errorMessages.isEmpty()) {
			throw new BusinessException(errorMessages);
		}

		List list = listMoviesByGenre(genre,order);
		return Response.status(Response.Status.OK).entity(list).build();
	}

	private List listMoviesByGenre(String genre, String order) {
		//Just return the list as is as this is a demo
		//We would use some logic to do the filtering and ordering in real world apps.
		return MOVIE_LIST;
	}

If you invoke the service without any of the required parameters, it would return the HTTP status code of 400 along with the detailed messages. This works because our BusinessException thrown during validation checks is processed by the ExceptionHttpStatusResolver which translates the exception to the correct Response code.

When a resource method throws an exception for which there is an exception mapping provider, the matching provider is used to obtain a Response instance. The resulting Response is processed as if the method throwing the exception had instead returned the Response.

Example invocation URL:
http://localhost:8080/jaxrs-movie-service/movie/list?order=dummy

The above should result in the below response:

Response headers:

  • Status Code: 400 Bad Request
  • Content-Type: application/json

Response body:

[
"genre is required",
"order of either ASC or DESC must be specified",
"genre of either Action or Drama must be specified"
]

Conclusion

Thus we can make use of JAXRS Exception Mapping providers for returning HTTP Status codes and add the needed error details in the response body as well.

Advertisements
Post a comment or leave a trackback: Trackback URL.

Comments

  • Raj Kumar  On January 3, 2014 at 4:41 pm

    Hi Prashant,

    i downloaded the src code but its not giving json response body for exception handling case. could you pls check it.

  • Mukesh  On February 11, 2014 at 6:35 pm

    Hi Prashant,
    I’m using Tomcat and I’m application is not giving json response body for exception handling case, instead it giving following exception:

    javax.ws.rs.WebApplicationException
    at com.sun.jersey.spi.container.ContainerResponse.write(ContainerResponse.java:268)
    at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1029)
    at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:941)
    at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:932)
    at com.sun.jersey.spi.container.servlet.WebComponent.service(WebComponent.java:384)
    at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:451)
    at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:632)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.netbeans.modules.web.monitor.server.MonitorFilter.doFilter(MonitorFilter.java:393)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:298)
    at org.apache.coyote.http11.Http11AprProcessor.process(Http11AprProcessor.java:859)
    at org.apache.coyote.http11.Http11AprProtocol$Http11ConnectionHandler.process(Http11AprProtocol.java:579)
    at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1555)
    at java.lang.Thread.run(Thread.java:722)

  • Thomas  On February 18, 2014 at 1:26 am

    Hi Prashant. Very useful article. Thank you.

    I found that as soon as I add an interceptor to the method from an exception is thrown, my exception handler no longer gets called. Thoughts? sample code is below:
    @ManagedBean
    @Path(“/clinicalStudyService”)
    public class ClinicalStudyService {

    @EJB
    private ClinicalStudyDataService clinicalStudyDataService;

    @Inject
    private ClinicalStudyValidator clinicalStudyValidator;

    @POST
    @Consumes(MediaType.TEXT_XML)
    // @Interceptors({ClinicalStudyBuilderInterceptor.class})
    public String addClinicalStudy(ClinicalStudy clinicalStudy) {
    System.out.println(“Start – ” + clinicalStudy.getId());

    clinicalStudyValidator.validate(clinicalStudy);
    clinicalStudyDataService.save(clinicalStudy);

    System.out.println(“End – ” + clinicalStudy.getId());
    return WebserviceResponseStatus.SUCCESS.getDescription();
    }
    }

    @ManagedBean
    public class ClinicalStudyValidator {

    public void validate(ClinicalStudy clinicalStudy) {
    if(StringUtils.isEmpty(clinicalStudy.getId())) {
    throw new WebServiceException(“NCT ID is required for Clinical Study.”);
    }
    }
    }

    @Provider
    public class ExceptionHttpStatusResolver implements ExceptionMapper {

    @Override
    public Response toResponse(WebServiceException webServiceException) {
    return Response.status(Response.Status.BAD_REQUEST).entity(webServiceException.getMessage()).build();
    }

    }

  • Johng87  On October 1, 2014 at 12:46 am

    As I web site possessor I believe the content material here is rattling great , appreciate it for your hard work. You should keep it up forever! Good Luck. fdabafccgded

  • lp  On February 14, 2015 at 7:26 pm

    Tanks a lot, very helpful article

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: