ClientHttpRequestFactory as abstraction over the HTTP layer

Anton Tkachenko
Duda
Published in
4 min readJun 11, 2024

Introduction

This article is part of a series of articles about a deeper dive into making HTTP calls in Spring.

What’s under the facade of RestTemplate

As I mentioned earlier, RestTemplate does not have its own implementation of the HTTP client. Instead, it uses the ClientHttpRequestFactory abstraction to delegate the actual HTTP calls to some "native" client.

For instance, the behaviour of MockRestServiceServer is based on the MockClientHttpRequestFactory that is set to the rest template instead of the "real" ClientHttpRequestFactory that will make the actual HTTP calls.

@Test
void aGlimpseIntoMockRestServiceServer() {
RestTemplate restTemplate = new RestTemplate();
// This logic is executed as part of @RestClientTest bootstrapping.
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
Assertions.assertThat(restTemplate.getRequestFactory().getClass().getSimpleName())
/**
* @{@link org.springframework.test.web.client.MockRestServiceServer#MockClientHttpRequestFactory}
* // no actual HTTP request is made, so we do whatever we want
* with expected "requests" and "responses"
*/
.isEqualTo("MockClientHttpRequestFactory");
}

Here is a visual comparison of what making a call to MockServer (or a remote server) looks in comparison with MockRestServiceServer:

In many examples on the internet, you will see the new RestTemplate() being used, and I do not recommend doing it (unless you are writing a small script or a prototype). One of the reasons is that the default request factory of RestTemplate is SimpleClientHttpRequestFactory that is a bit limited

@Test
void theDefaultRequestFactoryOfRestTemplateBehavior() {
RestTemplate restTemplate = new RestTemplate();

Assertions.assertThat(restTemplate.getRequestFactory())
.isInstanceOf(org.springframework.http.client.SimpleClientHttpRequestFactory.class);
// try and guess what will happen here and why the test will fail
restTemplate.patchForObject("https://api.example.com/v1/some-endpoint", null, String.class);
}

The beauty of RestTemplate and ClientHttpRequestFactory abstractions

Since RestTemplate and ClientHttpRequestFactory are 'natural' Spring-web abstractions, you can even integrate them in testing your server-side code. I'm pretty sure that you are familiar with the @WebMvcTest and MockMvc that allows you to test your controllers without starting the whole application context and underlying servers. But you might have noticed that MockMvc is often quite verbose when you need to test a simple HTTP call:

mockMvc.perform(
MockMvcRequestBuilders.get("/some-endpoint")
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().json("{\"name\":\"John\",\"age\":25}"));

I bet that when writing any test from scratch, you always search for the class that provides all the get(...), status() static methods. Also, since we delegate assertions to MockMvcResultMatchers, we lose the convenience of debugging that we have when we use assertThat from org.assertj.core.api.Assertions on the response.

Another example of MockMvc verbosity when you need to make a call with a request body and then read the response body into a POJO:

record ResApiResponse(String name) {
}

RestApiResponse response = new com.fasterxml.jackson.databind.ObjectMapper()
.readValue(
mockMvc.perform(
MockMvcRequestBuilders.post("/some-endpoint")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"John\"}")
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn()
.getResponse()
.getContentAsString(),
ResApiResponse.class
);

And now multiply this by the number of endpoints/cases that you usually need to test. And the sad fact is that many developers also do not know about TestRestTemplate / WebTestClient (but it's a different story).

But the good news is that you can use RestTemplate / RestClient as a client to make calls to your MockMvc-covered server-side code in the same way as you make calls to external services.

The trick is to define a RestTemplate bean in your test configuration that uses MockMvcClientHttpRequestFactory and the MockMvc instance that you get from the context

@Bean
RestOperations mockMvcRestOperations(MockMvc mockMvc) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new MockMvcClientHttpRequestFactory(mockMvc));
return restTemplate;
}

The benefit of this approach will show itself when you need to debug an environment-sensitive production issue covered by a long cross-endpoint behavioural unit test that is green. You can adjust the configuration of the RestTemplate bean to use the "real" HTTP client, do some tricks with API authentication - and voilà, a cross-environment test case is ready in a few minutes.

/**
* Illustrates how to use {@link MockMvcClientHttpRequestFactory} to test the client-side
* with RestTemplate, and not MockMvc.
*/
@WebMvcTest
public class Part03_02_ClientHttpRequestFactory {
// this is the "inlined" application we're going to test
@SpringBootApplication
static class TheSpringBootApplication {
record ResponseModel(String name, int age) {
}
// this is our server-side controller
@RestController
static class TheRestController {
@GetMapping("/api/v1/some-endpoint/{name}/{age}")
public ResponseModel getResponse(@PathVariable Integer age, @PathVariable String name) {
return new ResponseModel(name, age);
}
}
@Qualifier("mockMvcRestOperations")
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface MockMvcRestTemplate {
}
@MockMvcRestTemplate
@Bean
RestOperations mockMvcRestOperations(MockMvc mockMvc) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new MockMvcClientHttpRequestFactory(mockMvc));
return restTemplate;
}
// by default, RestTemplate throws exceptions when 4xx or 5xx status codes are returned
// but when we know that we expect such status codes, we can tweak the RestTemplate
// to not throw exceptions, so that to assert the response status ourselves
@Qualifier("nonThrowingRestOperations")
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface NonThrowingMockMvcRestTemplate {
}
@NonThrowingMockMvcRestTemplate
@Bean
RestOperations nonThrowingRestOperations(MockMvc mockMvc) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new MockMvcClientHttpRequestFactory(mockMvc));
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
protected void handleError(@NotNull ClientHttpResponse response, @NotNull HttpStatusCode statusCode) {
// we won't throw exceptions here, and instead we will let the flow handle the response status
}
});
return restTemplate;
}
}
@MockMvcRestTemplate
@Autowired
RestOperations restOperations;
@NonThrowingMockMvcRestTemplate
@Autowired
RestOperations nonThrowingRestOperations;
@Test
void callOurGetEndpoint_shouldReturnOurResponse() {
record ClientSideResponse(String name, int age) {
}
var response = restOperations.getForObject(
"/api/v1/some-endpoint/{name}/{age}",
ClientSideResponse.class,
"John", 25
);
assertThat(response.name()).isEqualTo("John");
assertThat(response.age()).isEqualTo(25);
assertThatExceptionOfType(HttpClientErrorException.NotFound.class)
.isThrownBy(() -> restOperations.getForObject("/api/v1/non-existing-endpoint", ClientSideResponse.class));
// now we can make a request to a non-existing endpoint without causing an exception
var fourOhFour = nonThrowingRestOperations.getForEntity(
"/api/v1/non-existing-endpoint", String.class
);
assertThat(fourOhFour.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(fourOhFour.getBody()).isNull();
}
}

The short conclusion of Part 3

In this article, we’ve seen how RestTemplate uses ClientHttpRequestFactory to delegate the actual calls to a real HTTP client or some mock server from test-kit. Also, now you know how you can use RestTemplate or RestClient test your controllers if you like its API more than MockMvc.

--

--