RestTemplate is deprecated. Use WebClient.

Anurag Rana
Naukri Engineering
Published in
6 min readMay 10, 2023
https://unsplash.com/photos/m_HRfLhgABo

RestTemplate has been deprecated in favor of the newer WebClient in Spring Framework 5.0 and later versions. This means that while the RestTemplate is still available for use, Spring developers are encouraged to migrate to WebClient for new projects.

There are several reasons why WebClient is preferred over RestTemplate:

  1. Non-blocking I/O: WebClient is built on top of Reactor, which provides a non-blocking, reactive approach to handling I/O. This allows for better scalability and performance in high-traffic applications.
  2. Functional style: WebClient uses a functional style of programming, which can make code easier to read and understand. It also provides a fluent API that makes it easier to configure and customize requests.
  3. Better support for streaming: WebClient supports streaming of request and response bodies, which can be useful for handling large files or real-time data.
  4. Improved error handling: WebClient provides better error handling and logging than RestTemplate, making it easier to diagnose and troubleshoot issues.
  5. Even if you upgrade the spring web 6.0.0 version, you won’t be able to set the request timeout in the HttpRequestFactory. This is one of the biggest factors to avoid using RestTemplate.
setting request timeout will have no effect.

Overall, while the RestTemplate may still work for some use cases, WebClient offers several advantages that make it a better choice for modern Spring applications

Let’s see how to use WebClient in your SpringBoot 3 Application.

(1) Create webclient:

import io.netty.channel.ChannelOption;
import io.netty.channel.ConnectTimeoutException;
import io.netty.handler.timeout.ReadTimeoutException;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.TimeoutException;
import jakarta.annotation.PostConstruct;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;


HttpClient httpClient =
HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout)
.responseTimeout(Duration.ofMillis(requestTimeout))
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(readTimeout)));

WebClient client =
WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();

(2) Sending requests Synchronously (Just like RestTemplate)

If you want to stick to the older method of sending HTTP requests and waiting for the response, this same can be achieved using WebClient as below.

public String postSynchronously(String url, String requestBody) {
LOG.info("Going to hit API - URL {} Body {}", url, requestBody);
String response = "";
try {
response =
client
.method(HttpMethod.POST)
.uri(url)
.accept(MediaType.ALL)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(String.class)
.block();

} catch (Exception ex) {
LOG.error("Error while calling API ", ex);
throw new RunTimeException("XYZ service api error: " + ex.getMessage());
} finally {
LOG.info("API Response {}", response);
}

return response;
}

block() is used to wait for the response synchronously, which may not be ideal in all cases. You may want to consider using subscribe() and handling the response asynchronously instead.

(3) Sending Request Asynchronously:

Sometimes we do not want to wait for the response and would like to process the response asynchronously. This can be done as below.

import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public static Mono<String> makePostRequestAsync(String url, String postData) {
WebClient webClient = WebClient.builder().build();
return webClient.post()
.uri(url)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData("data", postData))
.retrieve()
.bodyToMono(String.class);
}

To use this function, simply pass in the URL you want to send the POST request to and the data you want to send in the body of the request as a URL-encoded string. The function will return either the response from the server, or an error message if the request fails for any reason.

Note that in this example, the WebClient is built using the default configuration. You may need to configure it differently depending on your requirements. Also, note that block() is used to wait for the response synchronously, which may not be ideal in all cases. You may want to consider using subscribe() and handling the response asynchronously instead.

To consume the response, you can subscribe to the Mono and handle the response asynchronously. Here's an example:

makePostRequestAsync("https://example.com/api", "param1=value1&param2=value2")
.subscribe(response -> {
// handle the response
System.out.println(response);
}, error -> {
// handle the error
System.err.println(error.getMessage());
});

subscribe() is used to handle the response asynchronously. You can provide two lambda expressions as arguments to subscribe(). The first lambda expression is executed if the request succeeds and receives the response as an argument. The second lambda expression is executed if the request fails and receives the error as an argument.

(4) Handing 4XX and 5XX Errors:

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public static Mono<String> makePostRequestAsync(String url, String postData) {
WebClient webClient = WebClient.builder()
.baseUrl(url)
.build();
return webClient.post()
.uri("/")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData("data", postData))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Client error")))
.onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Server error")))
.bodyToMono(String.class);
}

In this example, the onStatus() method is called twice, once for 4xx client errors and once for 5xx server errors. Each call to onStatus() takes two arguments: a Predicate that determines whether the error status code matches the condition, and a Function that returns a Mono that emits the error to be propagated to the subscriber.

If the status code matches the condition, the corresponding Mono is emitted and the Mono chain terminates with an error. In this example, the Mono emits a RuntimeException with an error message that indicates whether the error was a client error or a server error.

(5) Taking action based on error status:

To take action based on errors in the subscribe method of the Mono, you can add another lambda expression to the subscribe method after the lambda expression that handles the response. This second lambda expression is executed if there is an error during the processing of the Mono.

Here’s an updated example of how to use the makePostRequestAsync function and handle errors in the subscribe method:

makePostRequestAsync("https://example.com/api", "param1=value1&param2=value2")
.subscribe(response -> {
// handle the response
System.out.println(response);
}, error -> {
// handle the error
System.err.println("An error occurred: " + error.getMessage());
if (error instanceof WebClientResponseException) {
WebClientResponseException webClientResponseException = (WebClientResponseException) error;
int statusCode = webClientResponseException.getStatusCode().value();
String statusText = webClientResponseException.getStatusText();
System.err.println("Error status code: " + statusCode);
System.err.println("Error status text: " + statusText);
}
});

the second lambda expression in the subscribe method checks if the error is an instance of WebClientResponseException, which is a specific type of exception thrown by WebClient when there is an error response from the server. If it is an instance of WebClientResponseException, the code extracts the status code and status text from the exception and logs them.

You can also add additional error handling logic in this lambda expression based on the specific error that occurred. For example, you can retry the request, fallback to a default value, or log the error in a specific way.

(6) Complete code for handling Success Response and Error:

responseMono.subscribe(
response -> {
// handle the response
LOG.info("SUCCESS API Response {}", response);
},
error -> {
// handle the error
LOG.error("An error occurred: {}", error.getMessage());
LOG.error("error class: {}", error.getClass());

// Errors / Exceptions from Server
if (error instanceof WebClientResponseException) {
WebClientResponseException webClientResponseException =
(WebClientResponseException) error;
int statusCode = webClientResponseException.getStatusCode().value();
String statusText = webClientResponseException.getStatusText();
LOG.info("Error status code: {}", statusCode);
LOG.info("Error status text: {}", statusText);
if (statusCode >= 400 && statusCode < 500) {
LOG.info(
"Error Response body {}", webClientResponseException.getResponseBodyAsString());
}

Throwable cause = webClientResponseException.getCause();
LOG.error("webClientResponseException");
if (null != cause) {
LOG.info("Cause {}", cause.getClass());
if (cause instanceof ReadTimeoutException) {
LOG.error("ReadTimeout Exception");
}
if (cause instanceof TimeoutException) {
LOG.error("Timeout Exception");
}
}
}

// Client errors i.e. Timeouts etc -
if (error instanceof WebClientRequestException) {
LOG.error("webClientRequestException");
WebClientRequestException webClientRequestException =
(WebClientRequestException) error;
Throwable cause = webClientRequestException.getCause();
if (null != cause) {
LOG.info("Cause {}", cause.getClass());
if (cause instanceof ReadTimeoutException) {
LOG.error("ReadTimeout Exception");
}

if (cause instanceof ConnectTimeoutException) {
LOG.error("Connect Timeout Exception");
}
}
}
});

Timeouts:

We can set timeout in each request as below:

return webClient
.method(this.httpMethod)
.uri(this.uri)
.headers(httpHeaders -> httpHeaders.addAll(additionalHeaders))
.bodyValue(this.requestEntity)
.retrieve()
.bodyToMono(responseType)
.timeout(Duration.ofMillis(readTimeout)) // request timeout for this request
.block();

Unfortunately, we can not set connect timeout in each request. That is the property of WebClient and can be set only once. We can always create a new webclient instance with new connect timeout values if required.

The difference between connect timeout, read timeout and request timeout is as below.

Conclusion:

Since RestTemplace is deprecated, developers should start using WebClient for making REST calls. Non-blocking I/O calls will surely improve the application performance. Not only does it provide many other exciting features like improved error handling and support for streaming, but it can also be used in blocking mode to simulate the RestTemplate behavior if needed.

--

--