1. Overview

A lot of frameworks and projects are introducing reactive programming and asynchronous request handling. As such, Spring 5 introduced a reactive WebClient implementation as part of the WebFlux framework.

In this tutorial, we’ll learn how to reactively consume REST API endpoints with WebClient.

2. REST API Endpoints

To start, let’s define a sample REST API with the following GET endpoints:

  • /products – get all products
  • /products/{id} – get product by ID
  • /products/{id}/attributes/{attributeId} – get product attribute by id
  • /products/?name={name}&deliveryDate={deliveryDate}&color={color} – find products
  • /products/?tag[]={tag1}&tag[]={tag2} – get products by tags
  • /products/?category={category1}&category={category2} – get products by categories

Here we defined a few different URIs. In just a moment, we’ll figure out how to build and send each type of URI with WebClient.

Please note that the URIs for gettings products by tags and categories contain arrays as query parameters; however, the syntax differs because there’s no strict definition of how arrays should be represented in URIs. This primarily depends on the server-side implementation. Accordingly, we’ll cover both cases.

3. WebClient Setup

First, we’ll need to create an instance of WebClient. For this article, we’ll be using a mocked object to verify that a valid URI is requested.

Let’s define the client and related mock objects:

exchangeFunction = mock(ExchangeFunction.class);
ClientResponse mockResponse = mock(ClientResponse.class);
when(mockResponse.bodyToMono(String.class))
  .thenReturn(Mono.just("test"));

when(exchangeFunction.exchange(argumentCaptor.capture()))
  .thenReturn(Mono.just(mockResponse));

webClient = WebClient
  .builder()
  .baseUrl("https://example.com/api")
  .exchangeFunction(exchangeFunction)
  .build();

We’ll also pass a base URL that will be prepended to all requests made by the client.

Finally, to verify that a particular URI has been passed to the underlying ExchangeFunction instance, we’ll use the following helper method:

private void verifyCalledUrl(String relativeUrl) {
    ClientRequest request = argumentCaptor.getValue();
    assertEquals(String.format("%s%s", BASE_URL, relativeUrl), request.url().toString());
    
    verify(this.exchangeFunction).exchange(request);
    verifyNoMoreInteractions(this.exchangeFunction);
}

The WebClientBuilder class has the uri() method that provides the UriBuilder instance as an argument. Generally, we make an API call in the following manner:

webClient.get()
  .uri(uriBuilder -> uriBuilder
    //... building a URI
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .onErrorResume(e -> Mono.empty())
  .block();

We’ll use UriBuilder extensively in this guide to construct URIs. It’s worth noting that we can build a URI using other methods, and then just pass the generated URI as a String.

4. URI Path Component

A path component consists of a sequence of path segments separated by a slash ( / ). First, we’ll start with a simple case where a URI doesn’t have any variable segments, /products:

webClient.get()
  .uri("/products")
  .retrieve()
  .bodyToMono(String.class)
  .onErrorResume(e -> Mono.empty())
  .block();

verifyCalledUrl("/products");

For this case, we can just pass a String as an argument.

Next, we’ll take the /products/{id} endpoint and build the corresponding URI:

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/{id}")
    .build(2))
  .retrieve()
  .bodyToMono(String.class)
  .onErrorResume(e -> Mono.empty())
  .block();

verifyCalledUrl("/products/2");

From the code above, we can see that the actual segment values are passed to the build() method.

In a similar way, we can create a URI with multiple path segments for the /products/{id}/attributes/{attributeId} endpoint:

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/{id}/attributes/{attributeId}")
    .build(2, 13))
  .retrieve()
  .bodyToMono(String.class)
  .onErrorResume(e -> Mono.empty())
  .block();

verifyCalledUrl("/products/2/attributes/13");

A URI can have as many path segments as required, though the final URI length must not exceed limitations. Finally, we need to remember to keep the right order of actual segment values passed to the build() method.

5. URI Query Parameters

Usually, a query parameter is a simple key-value pair like title=Baeldung. Let’s see how to build such URIs.

5.1. Single Value Parameters

We’ll start with single value parameters and take the /products/?name={name}&deliveryDate={deliveryDate}&color={color} endpoint. To set a query parameter, we’ll call the queryParam() method of the UriBuilder interface:

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "AndroidPhone")
    .queryParam("color", "black")
    .queryParam("deliveryDate", "13/04/2019")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .onErrorResume(e -> Mono.empty())
  .block();

verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");

Here we added three query parameters and assigned actual values immediately. Conversely, it’s also possible to leave placeholders instead of exact values:

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "{title}")
    .queryParam("color", "{authorId}")
    .queryParam("deliveryDate", "{date}")
    .build("AndroidPhone", "black", "13/04/2019"))
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13%2F04%2F2019");

This might be especially helpful when passing a builder object further in a chain.

Note that there’s one important difference between the two code snippets above. With attention to the expected URIs, we can see that they’re encoded differently. Particularly, the slash character ( / ) was escaped in the last example.

Generally speaking, RFC3986 doesn’t require the encoding of slashes in the query; however, some server-side applications might require such conversion. Therefore, we’ll see how to change this behavior later in this guide.

5.2. Array Parameters

We might need to pass an array of values, and there aren’t strict rules for passing arrays in a query string. Therefore, an array representation in a query string differs from project to project, and usually depends on underlying frameworks. We’ll cover the most widely used formats in this article.

Let’s start with the /products/?tag[]={tag1}&tag[]={tag2} endpoint:

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("tag[]", "Snapdragon", "NFC")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .onErrorResume(e -> Mono.empty())
  .block();

verifyCalledUrl("/products/?tag%5B%5D=Snapdragon&tag%5B%5D=NFC");

As we can see, the final URI contains multiple tag parameters, followed by encoded square brackets. The queryParam() method accepts variable arguments as values, so there’s no need to call the method several times.

Alternatively, we can omit square brackets and just pass multiple query parameters with the same key, but different values, /products/?category={category1}&category={category2}:

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("category", "Phones", "Tablets")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .onErrorResume(e -> Mono.empty())
  .block();

verifyCalledUrl("/products/?category=Phones&category=Tablets");

Finally, there’s one more extensively-used method to encode an array, which is to pass comma-separated values. Let’s transform our previous example into comma-separated values:

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("category", String.join(",", "Phones", "Tablets"))
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .onErrorResume(e -> Mono.empty())
  .block();

verifyCalledUrl("/products/?category=Phones,Tablets");

We’re just using the join() method of the String class to create a comma-separated string. We can also use any other delimiter that’s expected by the application.

6. Encoding Mode

Remember how we previously mentioned URL encoding?

If the default behavior doesn’t fit our requirements, we can change it. We need to provide a UriBuilderFactory implementation while building a WebClient instance. In this case, we’ll use the DefaultUriBuilderFactory class. To set encoding, we’ll call the setEncodingMode() method. The following modes are available:

  • TEMPLATE_AND_VALUES: Pre-encode the URI template and strictly encode URI variables when expanded
  • VALUES_ONLY: Do not encode the URI template, but strictly encode URI variables after expanding them into the template
  • URI_COMPONENTS: Encode URI component value after expending URI variables
  • NONE: No encoding will be applied

The default value is TEMPLATE_AND_VALUES. Let’s set the mode to URI_COMPONENTS:

DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT);
webClient = WebClient
  .builder()
  .uriBuilderFactory(factory)
  .baseUrl(BASE_URL)
  .exchangeFunction(exchangeFunction)
  .build();

As a result, the following assertion will succeed:

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "AndroidPhone")
    .queryParam("color", "black")
    .queryParam("deliveryDate", "13/04/2019")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .onErrorResume(e -> Mono.empty())
  .block();

verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");

And, of course, we can provide a completely custom UriBuilderFactory implementation to handle URI creation manually.

7. Conclusion

In this article, we learned how to build different types of URIs using WebClient and DefaultUriBuilder.

Along the way, we covered various types and formats of query parameters. Finally, we wrapped up by changing the default encoding mode of the URL builder.

As always, all of the code snippets from the article are available over on GitHub repository.