Why You Should Switch from RestTemplate to Spring’s Official WebClient

The article explains that RestTemplate is deprecated in Spring 5+, outlines WebClient’s non‑blocking I/O, functional style, streaming support and improved error handling, and provides step‑by‑step code examples for synchronous and asynchronous requests, timeout configuration, and error processing.

IT Niuke
IT Niuke
IT Niuke
Why You Should Switch from RestTemplate to Spring’s Official WebClient

In Spring Framework 5.0 and later, RestTemplate is deprecated and the officially recommended replacement is WebClient , which is built on Reactor and offers a non‑blocking, reactive approach to I/O, improving scalability and performance for high‑traffic applications.

Non‑blocking I/O : WebClient uses Reactor to handle I/O without blocking threads, enabling better scalability.

Functional style : The API follows a functional programming style, providing a fluent and readable way to configure requests.

Streaming support : It can stream request and response bodies, useful for large files or real‑time data.

Improved error handling : WebClient offers richer error handling and logging, making troubleshooting easier.

Key limitation : Even after upgrading to Spring Web 6.0.0, the request timeout cannot be set via HttpRequestFactory, which is a major reason to abandon RestTemplate.

The article then demonstrates how to use WebClient in a Spring Boot 3 project.

(1) Create the 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) 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 makes the request synchronous; for asynchronous usage, subscribe() should be considered.

(3) Asynchronous request

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);
}

Calling the method returns a Mono that can be subscribed to for handling the response or errors.

(4) Handling 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);
}

The onStatus callbacks receive a predicate to match status codes and a function that returns a Mono error, which terminates the reactive chain.

(5) Acting on errors in subscribe

makePostRequestAsync("https://example.com/api", "param1=value1¶m2=value2")
    .subscribe(response -> {
        System.out.println(response);
    }, 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);
        }
    });

The second lambda processes errors; if the error is a WebClientResponseException, the status code and text are extracted and logged.

(6) Full example of handling success and errors

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();
            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 reqEx = (WebClientRequestException) error;
            Throwable cause = reqEx.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");
                }
            }
        }
    });

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 can only be set once for the client; to use a different connection timeout, a new WebClient instance must be created.

Conclusion

Because RestTemplate is deprecated, developers should adopt WebClient for REST calls; its non‑blocking I/O improves performance, and it also offers enhanced error handling, streaming support, and the ability to operate in a blocking mode to mimic RestTemplate when needed.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpringReactiveError handlingresttemplateWebClient
IT Niuke
Written by

IT Niuke

Focused on IT technology sharing, original and innovative content. IT Niuke, we grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.