Using Spring WebClient Instead of RestTemplate: Benefits, Code Samples, and Error Handling
Spring WebClient, the reactive replacement for the deprecated RestTemplate in Spring 5+, offers non‑blocking I/O, functional style, streaming support, and improved error handling; this article explains its benefits, demonstrates synchronous and asynchronous request patterns, timeout configuration, and comprehensive error processing with code examples.
Background
In Spring Framework 5.0 and later, RestTemplate is deprecated and replaced by the newer WebClient . Although RestTemplate still works, Spring developers are encouraged to migrate to WebClient for new projects.
Why WebClient Is Preferred
Non‑blocking I/O : Built on Reactor, WebClient uses a reactive, non‑blocking approach that improves scalability and performance for high‑traffic applications.
Functional style : Offers a fluent, functional API that makes code easier to read and customize.
Streaming support : Handles request and response bodies as streams, useful for large files or real‑time data.
Improved error handling : Provides richer error information and logging, simplifying debugging.
Key point: Even after upgrading to Spring Web 6.0.0, you cannot set request timeout via HttpRequestFactory , which is a major reason to abandon RestTemplate.
Creating a WebClient Instance
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();Synchronous Request (RestTemplate‑like)
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; for fully reactive usage, consider subscribe() instead.
Asynchronous Request
public static Mono
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);
}Call the method and handle the response with subscribe() :
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());
});Handling 4xx/5xx Errors
public static Mono
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 callbacks examine the status code and propagate a RuntimeException with an appropriate message.
Timeout Configuration
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 per call
.block();Connection timeout can only be set once when the HttpClient is created; to change it you must build a new WebClient instance.
Comprehensive 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");
}
}
}
}
);Conclusion
Since RestTemplate is deprecated, developers should adopt WebClient for REST calls; its non‑blocking I/O improves performance, and it adds features such as enhanced error handling and streaming support while still allowing a blocking mode to mimic RestTemplate when needed.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.