Blog

Testing Exception Handling of Spring's REST Controllers

22.06.2021 by Manuel Gerding - 6 min read

This blog post gives you an effortless way to test whether your exception handling of Spring Boot's RestTemplate is working. In order to do so we leverage Chaos Engineering and this way can prevent the unattractive path of mocking a part of your system or cumbersome manual testing effort.

Spring Boot’s RestTemplate

Microservices and distributed applications are a rising trend in today’s software development practices. One thing for sure is that at some point two microservices have the need to communicate with each other. The simplest approach to achieve that is to use HTTP-based communication like REST (Representational State Transfer). To do that, we use in Spring Boot the RestTemplate as shown in the Listing below. In this example we reach an endpoint given as parameter (url) via HTTP GET and expect a list of type Product in the response body. This response is used in an orchestrated endpoint reachable via /products/ and collects various products from endpoints of different microservices (more context of the demo online shop can be found here.

@RestController
@RequestMapping("/products")
public class ProductsController {
   @Value("${rest.endpoint.hotdeals}")
   private String urlHotDeals;
   ...
   @GetMapping
   public Products getProducts() {
       Products products = new Products();
       products.setFashion(getProduct(urlFashion));
       products.setToys(getProduct(urlToys));
       products.setHotDeals(getProduct(urlHotDeals));
       return products;
   }

   private List<Product> getProduct(String url) {
       return restTemplate.exchange(
            url,
            HttpMethod.GET,
            null,
            productListTypeReference
        ).getBody();
   }

}

Well, obviously this code is very simplified and in reality we also need to think about the error case. What if the other system is not available? Or answers slowly? What if the response is different than expected? Or erroneous? How to verify that without changing the other system’s source code? Right now, the code will throw an (unhandled) exception. To be specific, a HttpClientErrorException in case of HTTP 4xx responses and a HttpServerErrorException in case of HTTP 5xx responses. In order to handle the exception appropriately Spring Boot offers multiple ways - as shown e.g. in the blog post of Baeldung about "Spring RestTemplate Error Handling".

Tests first, keep Code as it is

So, while the above described solution works, we haven’t considered the error handling for now. Before improving that we follow the paradigm of Test Driven Development of tests-first and would like to test it for real - without mocking or temporary code changes. Luckily, steadybit is able to change the behavior of a Java application at runtime via bytecode injection, without re-deploying anything and without the requirement of additional source code dependencies. Once the agents are installed, steadybit has covered the application as visible on our dashboard (see Figure below) and we can start with our first tests on unhandled exceptions.

steadybit has discovered our system

As steadybit has already discovered our system we can easily create a few experiments to check on the behavior. We start with testing what happens if one of the called endpoints (e.g. hotdeals at ${urlHotDeals}/products) throws an exception.

Step 1: Create Application Experiment

The first thing to do is to create and define a new experiment to provoke an erroneous endpoint. When using steadybit, this is pretty straightforward:

  1. We go to Experiments, choose to create a new Application Experiment (because we are injecting chaos on the level of the Java Virtual Machine).
  2. We give the experiment a useful name (e.g. "Gateway is able to handle Hot-Deals exceptions") and choose the Global area for now.
  3. We choose to attack the hotdeals application as visible below.
Create experiment with steadybit, specify target application
  1. Since we want to have a maximum effect we choose to have an impact of 100% at the following wizard step.
  2. We apply the "controller exception" attack at the next step to provoke an exception at GET-requests on the /products-endpoint of hotdeals (see Figure below).
Create experiment with steadybit, specify attack
  1. Finally, we can use the HTTP Status check of the monitoring section to verify that the Gateway`s endpoint of http://k8s.demo.steadybit.io/products always responds with a HTTP OK status within a timeout of 3 seconds (see Figure below). This is our desired behavior of always having products at the gateway even when one of the product-microservices is failing. Create the experiment by finalising the wizard via "save".
Create experiment with steadybit, specify HTTP state check

Step 2: Run Experiment to Check Behavior

Now it’s time to verify the status quo. By starting the experiment we can see what happens without the need to change anything in the source code or shutting down parts of the system (in this case hotdeals).

Run experiment with steadybit, failed HTTP state check

Well, okay, that was expected. The exception injected by steadybit in Hot-Deals lead to an erroneous response of the RestController-endpoint (see logs below). This affects Gateway`s current implementation by also returning an HTTP 500 (see Figure above). So, by injecting an exception at component 1 (Hot-Deals) and stopping it to work, we also caused an exception at the dependend component 2 (Gateway).

2021-06-15 17:35:19.389 ERROR Request processing failed; nested exception is:
 java.lang.RuntimeException: Exception injected by steadybit
     at com.steadybit.HotDealsRestController.getHotDeals(HotDealsRestController.java:28)
...
 2021-06-15 17:35:55.891 ERROR 500 Server Error for HTTP GET "/products"
  org.springframework.web.client.HttpServerErrorException$InternalServerError: 500 :
    [{"timestamp":"2021-06-15T17:35:55.888+00:00","status":500,... (5526 bytes)]
...

Improve Implementation

Our goal is now to fix the current implementation by adding proper exception handling and afterwards validate it via the created experiment. There are two general places to work on the issue: First one is to fix the code at Hot-Deals being able to provide a fallback instead of an HTTP 500, the second one is to fix the code at Gateway being able to work with erroneous product-responses and provide a proper fallback. We decide to fix it in the Gateway by applying the simplest solution possible: a try-catch-block as seen in Listing below.

private List<Product> getProduct(String url) {
   try {
       return restTemplate.exchange(
            url,
            HttpMethod.GET,
            null,
            productListTypeReference
        ).getBody();
   } catch (RestClientException e) {
       log.error("RestClientException while fetching products", e);
       return Collections.emptyList();
   }
}

Validate Improvement

Now that we have improved the implementation we definitely need to check whether it works as expected. For that we simply reuse the existing experiment and re-execute it. This time the experiment is successful since the /product-endpoint always returns a HTTP OK with a valid JSON. In case hotdeals is erroneous it’s content is simply replaced by an empty list.

Run experiment with steadybit, successful HTTP state check

Conclusion

In this blog post we have tested and also improved the error handling for a synchronous HTTP request. By using steadybit we were able to verify the different error states without changing any source code or shutting down parts of the system. However, the current implementation is still very simplistic. In a real world use case you may need a more advanced solution as for instance a circuit breaker. Thereby, the gateway-component is reducing the amount of requests forwarded to hotdeals and reduces the load on hotdeals. This is especially desirable when the reason of hotdeals' exception is related to increased load. Checkout the implementation of that endpoint in GitHub and verify it with the created experiment as well.

Are we safe now?

The short answer to this question is: no. There is definitely more to test and we will cover these in future blog posts. Stay tuned!

Written by

Manuel Gerding, Product Manager

Manuel is the youngster in the steadybit family and is constantly hungry for knowledge and new perspectives to broaden his horizons. After working almost a decade as a consultant and software engineer he focusses his perspective on the needs and demands of the user. His mission is to build a great product that really makes customer’s services more stable and valuable to their customers. To regain energy, Manuel loves to read a good book, take a trip with his bike or do a short meditation session.
@manuelgerding Manuel Gerding

Recent articles

  • A common Pitfall of Spring Boot's RestTemplate

    One of the most fundamental advantages of Spring Boot is the paradigm of convention over configuration reducing the overhead heavily. Even so, there is a tiny p

    Read
  • Problem first: User Centricity at steadybit

    To build successful products, you can't get past user-centricity. Especially true for a product-based startup like steadybit. This blog post covers how we work

    Read
  • Top 3 Kubernetes Weak Spots affecting your Availability

    This blog post gives you an overview of the top 3 weak spots in Kubernetes and demonstrates an example to show how Chaos Engineering can be used to validate whe

    Read