Backend Development 13 min read

Migrating from RestTemplate to WebClient in Spring Framework: Benefits and Implementation Guide

This article explains why RestTemplate is deprecated in Spring Framework 5+, outlines the advantages of the reactive WebClient such as non‑blocking I/O, functional style, streaming and improved error handling, and provides complete Java code examples for creating, configuring, and using WebClient synchronously and asynchronously, including timeout and error management.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Migrating from RestTemplate to WebClient in Spring Framework: Benefits and Implementation Guide

In Spring Framework 5.0 and later, RestTemplate is deprecated and the newer reactive WebClient is recommended for REST calls.

WebClient offers several advantages: non‑blocking I/O built on Reactor for better scalability, a functional programming style with a fluent API, native support for streaming request/response bodies, and improved error handling and logging.

Note: even after upgrading to Spring Web 6.0.0, request timeout cannot be set in HttpRequestFactory , which is a key reason to abandon RestTemplate.

(1) Create a WebClient instance

import io.netty.channel.ChannelOption;
import io.netty.channel.ConnectTimeoutException;
import io.netty.channel.handler.timeout.ReadTimeoutException;
import io.netty.channel.handler.timeout.ReadTimeoutHandler;
import io.netty.channel.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 forces a synchronous wait; for fully reactive usage you may prefer subscribe() instead.

(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
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 method with the target URL and URL‑encoded form data; the returned Mono will emit the response or an error.

In this example the WebClient is built with default settings; adjust the configuration as needed. Remember that block() is synchronous and may not suit all scenarios; consider using subscribe() for asynchronous handling.

(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
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 calls inspect the response status; the first argument is a predicate that matches the status code, and the second supplies a Mono that propagates an error.

aPredicate determines whether the status matches the condition

aFunction returns a Mono containing the error information

(5) Acting on error status in subscribe()

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 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 checks whether the error is a WebClientResponseException and logs the status code and text; additional actions such as retries or fallback can be added.

(6) Full example of handling success 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());

        // Server side errors
        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");
                }
            }
        }

        // Client side errors such as timeouts
        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");
                }
            }
        }
    });

Timeout

Per‑request timeout can be set as follows:

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 cannot be set per request; it is a property of the WebClient and must be configured when the client is created.

Image illustrating the difference between connection timeout, read timeout, and request timeout.

Conclusion

Because RestTemplate is deprecated, developers should adopt WebClient for REST calls; its non‑blocking I/O improves performance, and it adds features such as better error handling and streaming support, while still being usable in a blocking style to mimic RestTemplate behavior.

Final note (please follow)

If this article helped you, consider liking, watching, sharing, or bookmarking it.

JavaSpringHTTPReactiveRestTemplateWebClientErrorHandling
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login 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.