Why WebClient Beats RestTemplate in Spring: Non‑Blocking I/O, Functional API, and Advanced Error Handling
This article explains why Spring's RestTemplate is deprecated in favor of WebClient, highlighting benefits such as non‑blocking I/O, a functional programming style, streaming support, improved error handling, and how to configure timeouts, with full code examples for synchronous and asynchronous requests.
In Spring Framework 5.0 and later, RestTemplate is deprecated and replaced by the newer WebClient. Although RestTemplate remains usable, Spring developers are encouraged to migrate new projects to WebClient.
WebClient outperforms RestTemplate for several reasons:
Non‑blocking I/O : Built on Reactor, WebClient provides a reactive, non‑blocking approach to I/O, offering better scalability and performance for high‑traffic applications.
Functional style : WebClient uses a functional programming style, making code easier to read and configure with a fluent API.
Better streaming support : It supports streaming request and response bodies, useful for large files or real‑time data.
Improved error handling : WebClient offers superior error handling and logging, simplifying diagnosis and resolution of issues.
Key point: Even after upgrading to Spring Web 6.0.0, the inability to set request timeout in HttpRequestFactory is a major reason to abandon RestTemplate.
Overall, while RestTemplate may still suit some use cases, WebClient provides several advantages that make it the better choice for modern Spring applications.
“Let’s see how to use WebClient in a SpringBoot 3 application.”
1) Create the WebClient
import io.netty.channel.ChannelOption;
import io.netty.channel.ConnectTimeoutException;
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) Synchronous request (similar to RestTemplate)
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;
}The block() call waits synchronously for the response, which may not suit all scenarios; consider using subscribe() for asynchronous handling.
3) Asynchronous request
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);
}Invoke the function with the target URL and URL‑encoded data; it returns the server response or an error message.
Note that the WebClient in this example is built with default configuration; you may need to customize it for specific requirements. Also, block() is synchronous and may not be appropriate for all cases—consider subscribe() for asynchronous processing.
To handle the response asynchronously, subscribe to the returned Mono:
makePostRequestAsync("https://example.com/api", "param1=value1¶m2=value2")
.subscribe(response -> {
// handle the response
System.out.println(response);
}, error -> {
// handle the error
System.err.println(error.getMessage());
});4) Handling 4xx and 5xx errors
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);
}The onStatus method is called twice: once for 4xx client errors and once for 5xx server errors. Each call receives a predicate to match the status code and a function that returns a Mono emitting the error.
5) Acting on error status
makePostRequestAsync("https://example.com/api", "param1=value1¶m2=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 ex = (WebClientResponseException) error;
int statusCode = ex.getStatusCode().value();
String statusText = ex.getStatusText();
System.err.println("Error status code: " + statusCode);
System.err.println("Error status text: " + statusText);
}
});This lambda checks whether the error is a WebClientResponseException (thrown for server error responses) and logs the status code and text, allowing further custom error handling such as retries or fallbacks.
6) Complete error‑handling example
responseMono.subscribe(
response -> {
LOG.info("SUCCESS API Response {}", response);
},
error -> {
LOG.error("An error occurred: {}", error.getMessage());
LOG.error("error class: {}", error.getClass());
if (error instanceof WebClientResponseException) {
WebClientResponseException ex = (WebClientResponseException) error;
int statusCode = ex.getStatusCode().value();
String statusText = ex.getStatusText();
LOG.info("Error status code: {}", statusCode);
LOG.info("Error status text: {}", statusText);
if (statusCode >= 400 && statusCode < 500) {
LOG.info("Error Response body {}", ex.getResponseBodyAsString());
}
Throwable cause = ex.getCause();
LOG.error("webClientResponseException");
if (cause != null) {
LOG.info("Cause {}", cause.getClass());
if (cause instanceof ReadTimeoutException) {
LOG.error("ReadTimeout Exception");
}
if (cause instanceof TimeoutException) {
LOG.error("Timeout Exception");
}
}
}
if (error instanceof WebClientRequestException) {
LOG.error("webClientRequestException");
WebClientRequestException ex = (WebClientRequestException) error;
Throwable cause = ex.getCause();
if (cause != null) {
LOG.info("Cause {}", cause.getClass());
if (cause instanceof ReadTimeoutException) {
LOG.error("ReadTimeout Exception");
}
if (cause instanceof ConnectTimeoutException) {
LOG.error("Connect Timeout Exception");
}
}
}
}
);7) Timeout configuration per request
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();Connection timeout, read timeout, and request timeout differ: connection timeout is a property of the WebClient instance and can be set only once; a new client must be created to use a different connection timeout.
Conclusion
Since RestTemplate is deprecated, developers should start using WebClient for REST calls. Its non‑blocking I/O improves application performance, and it offers many exciting features such as enhanced error handling and streaming support, while still being usable in a blocking mode to mimic RestTemplate behavior when needed.
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
