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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Creating a WebClient Instance
Register a WebClientWebClient.Builder
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();
}
}
WebClient.BuilderWebClientGET Requests
Use retrieve()bodyToMono()bodyToFlux()
@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()body(BodyInserters.fromValue())
// 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()WebClientResponseExceptiononStatus()
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()Authorizationheaders()
// 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().retryWhen()
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.
/*
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.
*/