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.
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.
Click “Next” and enter the information as Figure 3.
Click “Next” and add “Spring Web” and “Jersey” dependencies as Figure 4
Click “Finish” to complete the creation steps.
3.2 Project Structure
In this step, I will show the created project structure as Figure 5.
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
orServerErrorException
. 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 SpringRestExceptionDemoApplication
class. 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 byWebApplicationExceptionMapper
‘stoResponse
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.
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.
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
You can download the full source code of this example here: How to Handle Exceptions in Rest Service