Spring WebClient: Reactive HTTP Client

WebClient is Spring's non-blocking, reactive HTTP client, introduced in Spring 5 as a modern replacement for RestTemplate. It uses Project Reactor under the hood and returns Mono (0–1 item) or Flux (0–N items) publishers. You can use WebClient in both reactive (WebFlux) and traditional (Spring MVC) applications: in MVC you simply call .block() to wait for the result.

Contents

Dependency & Setup

WebClient is included in spring-boot-starter-webflux. You can also add it to a Spring MVC project without switching to a fully reactive stack.

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Creating a WebClient Instance

Register a WebClient bean with a base URL. The WebClient.Builder approach lets you add default headers and codecs centrally.

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient orderServiceClient(WebClient.Builder builder) {
        return builder
            .baseUrl("https://api.example.com")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader(HttpHeaders.ACCEPT,       MediaType.APPLICATION_JSON_VALUE)
            .build();
    }
}
Inject WebClient.Builder (not WebClient directly) into your config so Spring Boot can apply any auto-configured filters, such as the Actuator metrics filter, automatically.

GET Requests

Use retrieve() for straightforward response handling. Chain bodyToMono() for a single object or bodyToFlux() for a collection.

Java
@Service
public class OrderClient {

    private final WebClient client;

    public OrderClient(WebClient orderServiceClient) {
        this.client = orderServiceClient;
    }

    // Reactive — returns Mono
    public Mono<Order> getOrderById(String id) {
        return client.get()
            .uri("/orders/{id}", id)
            .retrieve()
            .bodyToMono(Order.class);
    }

    // Reactive — returns Flux (streaming list)
    public Flux<Order> getAllOrders() {
        return client.get()
            .uri("/orders")
            .retrieve()
            .bodyToFlux(Order.class);
    }

    // Blocking (use in Spring MVC context)
    public Order getOrderBlocking(String id) {
        return getOrderById(id).block();
    }

    // With query parameters
    public Flux<Order> searchOrders(String status) {
        return client.get()
            .uri(uriBuilder -> uriBuilder
                .path("/orders")
                .queryParam("status", status)
                .build())
            .retrieve()
            .bodyToFlux(Order.class);
    }
}

POST Requests

Use bodyValue() to send a plain Java object (serialized to JSON) or body(BodyInserters.fromValue()) for reactive publishers.

Java
// POST a new order and get back the created resource
public Mono<Order> createOrder(Order order) {
    return client.post()
        .uri("/orders")
        .bodyValue(order)             // serialized to JSON automatically
        .retrieve()
        .bodyToMono(Order.class);
}

// PUT — update
public Mono<Order> updateOrder(String id, Order order) {
    return client.put()
        .uri("/orders/{id}", id)
        .bodyValue(order)
        .retrieve()
        .bodyToMono(Order.class);
}

// DELETE — returns Void (empty response)
public Mono<Void> deleteOrder(String id) {
    return client.delete()
        .uri("/orders/{id}", id)
        .retrieve()
        .bodyToMono(Void.class);
}

Error Handling

By default retrieve() throws WebClientResponseException for 4xx/5xx responses. Use onStatus() for custom error handling per status code.

Java
public Mono<Order> getOrderById(String id) {
    return client.get()
        .uri("/orders/{id}", id)
        .retrieve()
        .onStatus(HttpStatusCode::is4xxClientError, response ->
            response.bodyToMono(String.class)
                .flatMap(body -> Mono.error(new OrderNotFoundException("Order not found: " + id)))
        )
        .onStatus(HttpStatusCode::is5xxServerError, response ->
            Mono.error(new ServiceUnavailableException("Order service unavailable"))
        )
        .bodyToMono(Order.class);
}

// Fallback value on error
public Mono<Order> getOrderWithFallback(String id) {
    return getOrderById(id)
        .onErrorReturn(OrderNotFoundException.class, Order.empty())
        .onErrorMap(ServiceUnavailableException.class, e ->
            new RuntimeException("Downstream failure", e));
}

Headers & Authentication

Add per-request headers using header(). For Bearer token authentication, set the Authorization header or use the headers() consumer.

Java
// Per-request Bearer token
public Mono<Order> getOrder(String id, String token) {
    return client.get()
        .uri("/orders/{id}", id)
        .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
        .retrieve()
        .bodyToMono(Order.class);
}

// Default auth on all requests via defaultHeaders in config
WebClient.builder()
    .baseUrl("https://api.example.com")
    .defaultHeaders(headers -> headers.setBearerAuth(tokenProvider.getToken()))
    .build();

// Basic auth
WebClient.builder()
    .baseUrl("https://api.example.com")
    .defaultHeaders(headers -> headers.setBasicAuth("username", "password"))
    .build();

Timeouts & Retry

Configure response timeouts at the client level via the underlying Netty connector. Use .timeout() operator or .retryWhen() for retry logic.

Java
import io.netty.channel.ChannelOption;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;

// Connection and response timeout via Netty HttpClient
HttpClient httpClient = HttpClient.create()
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
    .responseTimeout(Duration.ofSeconds(10));

WebClient client = WebClient.builder()
    .baseUrl("https://api.example.com")
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .build();

// Timeout on individual request
client.get()
    .uri("/orders/{id}", id)
    .retrieve()
    .bodyToMono(Order.class)
    .timeout(Duration.ofSeconds(5));

// Retry with exponential backoff
client.get()
    .uri("/orders/{id}", id)
    .retrieve()
    .bodyToMono(Order.class)
    .retryWhen(Retry.backoff(3, Duration.ofMillis(500))
        .filter(ex -> ex instanceof ServiceUnavailableException));

WebClient vs RestTemplate

RestTemplate is still supported but in maintenance mode since Spring 5. WebClient is the recommended replacement for all new code.

Java
/*
  RestTemplate                    WebClient
  ─────────────────────────────────────────────────────────
  Blocking (one thread per call)  Non-blocking (event loop)
  Simpler API for CRUD            Fluent, composable API
  In maintenance mode             Actively developed
  Works in Spring MVC only        Works in MVC + WebFlux
  No built-in retry/backoff       retryWhen() built in
  No streaming support            bodyToFlux() for streams
  ─────────────────────────────────────────────────────────

  Migration: call .block() on Mono/Flux to get the same
  synchronous behaviour as RestTemplate while you migrate.
*/