Enterprise Java

How to Handle Exceptions in Rest Service

In this article, we will explain how to Handle Exceptions in Rest Service.

1. Introduction

Rest Service is a lightweight service that is built on the REpresentational State Transfer (REST) architecture via HTTP protocol. HTTP status code defines 4xx and 5xx error codes. Java API for RESTFul Web services (JAX-RS) specification is defined by Java EE with a set of interfaces and annotations to create a Rest service. JAX-RS runtime implements these interfaces. JAX-RS defines a set of exceptions that map the exception into the response object.

rest exceptions - jax-rs
Figure 1 Jax-RS Exceptions

In this example, I will build a spring boot application with Jersey library to demonstrate how to handle exceptions in a Rest service.

2. Technologies Used

The example code in this article was built and run using:

  • Java 11
  • Maven 3.3.9
  • Spring boot 2.4.5
  • Jersey 2.32
  • STS 4.10
  • Junit 5

3. Spring boot Project

In this step, I will create a Spring boot project via STS project wizard.

3.1 New Project

In this step, I will create a new Spring Starter project via STS.

First launch STS workspace. Then click New->Project and select “Spring Starter Project” wizard as Figure 2.

rest exceptions - spring starter
Figure 2 New Spring Starter Project

Click “Next” and enter the information as Figure 3.

Figure 3 Spring Rest Exception Demo Project

Click “Next” and add “Spring Web” and “Jersey” dependencies as Figure 4

Figure 4 Add Spring Web and Jersey Dependencies

Click “Finish” to complete the creation steps.

3.2 Project Structure

In this step, I will show the created project structure as Figure 5.

Figure 5 Project Structure

No need to change any generated files. You can start the spring boot application at this moment without any error.

3.3 Dependencies

The pom.xml is generated from STS workspace which includes spring-boot-starter-jersey and spring-boot-starter-web.

Pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>jcg.zheng.demo</groupId>
	<artifactId>spring-rest-exception-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-rest-exception-demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jersey</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

4. Rest Service with Exception

In this step, I will create a simple Rest service which handles the exception in two ways:

  • throw an exception which extends from ClientErrorException or ServerErrorException. The exception class’s constructor constructs the Rest service’s response object.
  • throw a runtime exception and handle it by an ExceptionMapper's toResponse method.

4.1 HandledDuplicateException

In this step, I will create a HandledDuplicateException class which extends from javax.ws.rs.ClientErrorException. The constructor builds the response object

HandledDuplicateException.java

package jcg.zheng.demo.springboot.exception;

import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

public class HandledDuplicateException extends ClientErrorException {
	private static final long serialVersionUID = 1L;

	public HandledDuplicateException(String message) {
		super(Response.status(Response.Status.CONFLICT).entity(message).type(MediaType.TEXT_PLAIN).build());
	}

}

4.2 HandledInternalException

In this step, I will create a HandledInternalException class which extends from javax.ws.rs.ServerErrorException. The constructor builds the response object.

HandledInternalException.java

package jcg.zheng.demo.springboot.exception;

import javax.ws.rs.ServerErrorException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

public class HandledInternalException extends ServerErrorException {
	private static final long serialVersionUID = 1L;

	public HandledInternalException(String message) {
		super(Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(message).type(MediaType.TEXT_PLAIN)
				.build());
	}

}

4.3 WebApplicationExceptionMapper

In this step, I will create a WebApplicationExceptionMapper class which extends from Exception and implements javax.ws.rs.ext.ExceptionMapper. I will override the toResponse method to handle the exceptions by building the appropriate response object.

WebApplicationExceptionMapper.java

package jcg.zheng.demo.springboot.exception;

import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class WebApplicationExceptionMapper extends RuntimeException implements ExceptionMapper<Exception> {
	private static final long serialVersionUID = 1L;

	public WebApplicationExceptionMapper() {
		super("Not found");
	}

	public WebApplicationExceptionMapper(String message) {
		super(message);
	}

	@Override
	public Response toResponse(Exception exception) {
		
		if (exception instanceof NotFoundException) {
			return Response.status(Response.Status.NOT_FOUND).entity(exception.getMessage()).type(MediaType.TEXT_PLAIN)
					.build();
		}

		if (exception instanceof BadRequestException) {
			return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage())
					.type(MediaType.TEXT_PLAIN).build();
		}

		return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(exception.getMessage())
				.type(MediaType.TEXT_PLAIN).build();

	}

}

4.4 HelloService

In this step, I will create a HelloService which has a Get service. The service first validates the data and processes the data. It throws exception at both validate and process methods. Then the exceptions are handled by HandledDuplicateException, HandledInternalException, or WebAppplicaitonExceptionMapper.

HelloService.java

package jcg.zheng.demo.springboot;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import org.springframework.stereotype.Service;

import jcg.zheng.demo.springboot.exception.HandledDuplicateException;
import jcg.zheng.demo.springboot.exception.HandledInternalException;

@Service
@Path("/hello")
public class HelloService {

	@GET
	@Produces("text/plain")
	public String hello(@QueryParam("name") @NotNull @Size(min = 3, max = 10) String name) {

		validate(name);

		return process(name);
	}

	private void validate(String name) {
		if ("Duplicate".equalsIgnoreCase(name)) {
			throw new HandledDuplicateException("duplicate request for " + name);
		}

		if ("Internal".equalsIgnoreCase(name)) {
			throw new HandledInternalException("Internal error " + name);
		}

		if ("NotFound".equalsIgnoreCase(name)) {
			throw new NotFoundException(name);
		}
		
		if ("BadRequest".equalsIgnoreCase(name)) {
			throw new BadRequestException(name);
		}

		process(name);
	}

	private String process(String name) {
		if ("Bad".equalsIgnoreCase(name)) {
			Integer.parseInt(name);
		}
		return "Hello " + name;
	}

}

4.5 JerseyConfiguration

In this step, I will create a JerseyConfiguration class which extends from org.glassfish.jersey.server.ResourceConfig. This is needed for the Jersey JAX-RS Runtime server.

JerseyConfiguration.java

package jcg.zheng.demo.springboot;

import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.context.annotation.Configuration;

import jcg.zheng.demo.springboot.exception.WebApplicationExceptionMapper;

@Configuration
public class JerseyConfiguration extends ResourceConfig {
	public JerseyConfiguration() {
		register(HelloService.class);
		register(WebApplicationExceptionMapper.class);
	}
}

4.6 SpringRestExceptionDemoApplication

In this step, I will include the generated SpringRestExceptionDemoApplicationclass. No change made to it.

SpringRestExceptionDemoApplication.java

package jcg.zheng.demo.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringRestExceptionDemoApplication {

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

}

5. Demo

5.1 Unit Test

In this step, I will add additional tests at the generated test class: SpringRestExceptionDemoApplicationTests.

  • test_happy_path – it sends data that returns a 200 ok response.
  • test_duplicate -it sends data which throws HandledDuplicateException that maps to 409 responses.
  • test_internal_handled – it sends data which throws HandledInternalException that maps to 500 internal server error.
  • test_internal_runtime – it sends data that throws a runtime exception. The exception is handled by WebApplicationExceptionMapper’s toResponse method.
  • test_not_found_data – it sends data that throws a NotFoundException. The exception is handled by WebApplicationExceptionMapper’s toResponse method.
  • test_not_found_path – it sends data that miss the required data and throws NotFoundException. The exception is handled by WebApplicationExceptionMapper’s toResponsemethod.
  • test_size_too_long – it sends a larger than expected size data. The exception is handled by WebApplicationExceptionMapper’s toResponse method.
  • test_bad_request – it sends data that throws BadRequestException. The exception is handled by WebApplicationExceptionMapper‘s toResponse method.

SpringRestExceptionDemoApplicationTests.java

package jcg.zheng.demo.springboot;

import static org.assertj.core.api.Assertions.assertThat;

import java.net.URI;
import java.net.URISyntaxException;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SpringRestExceptionDemoApplicationTests {

	@Autowired
	private TestRestTemplate restTemplate;

	@LocalServerPort
	int serverPort;

	@Test
	void contextLoads() {
	}

	private String getBaseUrl() {
		return "http://localhost:" + serverPort;
	}

	@Test
	public void test_bad_request() throws URISyntaxException {
		URI uri = new URI(getBaseUrl() + "/hello?name=BadRequest");

		ResponseEntity<String> ret = this.restTemplate.getForEntity(uri, String.class);
		assertThat(ret.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
		assertThat(ret.getBody()).isEqualTo("BadRequest");
	}

	@Test
	public void test_duplicate() throws URISyntaxException {
		URI uri = new URI(getBaseUrl() + "/hello?name=Duplicate");

		ResponseEntity<String> ret = this.restTemplate.getForEntity(uri, String.class);
		assertThat(ret.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
		assertThat(ret.getBody()).isEqualTo("duplicate request for Duplicate");
	}

	@Test
	public void test_happy_path() throws URISyntaxException {
		URI uri = new URI(getBaseUrl() + "/hello?name=Mary");

		ResponseEntity<String> ret = this.restTemplate.getForEntity(uri, String.class);
		assertThat(ret.getStatusCode()).isEqualTo(HttpStatus.OK);
		assertThat(ret.getBody()).isEqualTo("Hello Mary");
	}

	@Test
	public void test_internal_handled() throws URISyntaxException {
		URI uri = new URI(getBaseUrl() + "/hello?name=Internal");

		ResponseEntity<String> ret = this.restTemplate.getForEntity(uri, String.class);
		assertThat(ret.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
		assertThat(ret.getBody()).isEqualTo("Internal error Internal");
	}

	@Test
	public void test_internal_runtime() throws URISyntaxException {
		URI uri = new URI(getBaseUrl() + "/hello?name=Bad");

		ResponseEntity<String> ret = this.restTemplate.getForEntity(uri, String.class);
		assertThat(ret.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
		assertThat(ret.getBody()).isEqualTo("For input string: \"Bad\"");
	}

	@Test
	public void test_not_found_data() throws URISyntaxException {
		URI uri = new URI(getBaseUrl() + "/hello?name=NotFound");

		ResponseEntity<String> ret = this.restTemplate.getForEntity(uri, String.class);
		assertThat(ret.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
	}
	
	@Test
	public void test_not_found_path() throws URISyntaxException {
		URI uri = new URI(getBaseUrl() + "/hello?");

		ResponseEntity<String> ret = this.restTemplate.getForEntity(uri, String.class);
		assertThat(ret.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
		assertThat(ret.getBody()).isEqualTo("HTTP 404 Not Found");
	}
	
	@Test
	public void test_size_too_long() throws URISyntaxException {
		URI uri = new URI(getBaseUrl() + "/hello?name=toolongtoolongtoolong");

		ResponseEntity<String> ret = this.restTemplate.getForEntity(uri, String.class);
		assertThat(ret.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
		assertThat(ret.getBody()).isEqualTo("HTTP 404 Not Found");
	}

}

Execute the test command and capture the output here.

mvn test -DTest=SpringRestExceptionDemoApplicationTests output

[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 19.04 s - in jcg.zheng.demo.springboot.SpringRestExceptionDemoApplicationTests
2021-05-09 08:07:35.062  INFO 17700 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:05 min
[INFO] Finished at: 2021-05-09T08:07:35-05:00
[INFO] ------------------------------------------------------------------------

C:\MaryZheng\sts_4_10_ws\spring-rest-exception-demo>

5.2 Integration Test

In this step, I will start the spring boot application and send the http requests at Postman to demonstrate two type of exceptions.

  • 409 conflict request
  • 500 internal server error

5.2.1 Internal Server Error

Open postman, enter http://localhost:8080/hello?name=Bad. Click Send and you will see 500 Internal Server Error.

Figure 6 Internal Server Error

5.2.2 Conflict Request

Still in Postman, change the URL to http://localhost:8080/hello?name=Duplicate. Click Send and you will see 409 conflict request.

Figure 7 Conflict Request

6. Summary

I demonstrated how to handle exceptions in a Rest service in two ways. One is throwing an exception which extends from WebApplicationException and constructs the response inside the constructor. Another way is creating an implementation class of the ExceptionMapper interface.

7. Download the Source Code

Download
You can download the full source code of this example here: How to Handle Exceptions in Rest Service

Mary Zheng

Mary has graduated from Mechanical Engineering department at ShangHai JiaoTong University. She also holds a Master degree in Computer Science from Webster University. During her studies she has been involved with a large number of projects ranging from programming and software engineering. She works as a senior Software Engineer in the telecommunications sector where she acts as a leader and works with others to design, implement, and monitor the software solution.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button