WebClient & RestClient
Spring offers two modern HTTP clients with a shared, fluent builder style: WebClient (reactive, non-blocking) and RestClient (synchronous, introduced in Spring Framework 6.1). Both supersede RestTemplate. This page covers building requests, the retrieve and exchange styles, error handling, and timeouts for each.
RestClient — synchronous and modern
RestClient gives you a fluent API with a blocking model, ideal for traditional Spring MVC services. It needs no reactive dependency.
import org.springframework.context.annotation.*;
import org.springframework.web.client.RestClient;
@Configuration
public class HttpClientConfig {
@Bean
public RestClient userApiClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader("Accept", "application/json")
.build();
}
}
GET and POST with retrieve:
public record User(Long id, String name, String email) {}
public record CreateUser(String name, String email) {}
@Service
public class UserClient {
private final RestClient client;
public UserClient(RestClient userApiClient) {
this.client = userApiClient;
}
public User getUser(Long id) {
return client.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);
}
public User create(CreateUser body) {
return client.post()
.uri("/users")
.contentType(MediaType.APPLICATION_JSON)
.body(body)
.retrieve()
.body(User.class);
}
}
Output:
{ "id": 101, "name": "Grace Hopper", "email": "[email protected]" }
WebClient — reactive and non-blocking
WebClient comes from spring-boot-starter-webflux and returns Mono/Flux. Use it in reactive apps or when you need concurrent, non-blocking calls.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.*;
@Service
public class ReactiveUserClient {
private final WebClient client;
public ReactiveUserClient(WebClient.Builder builder) {
this.client = builder.baseUrl("https://api.example.com").build();
}
public Mono<User> getUser(Long id) {
return client.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class);
}
public Flux<User> listUsers() {
return client.get()
.uri("/users")
.retrieve()
.bodyToFlux(User.class);
}
}
Tip: You can block a
WebClientcall with.block(), but if you only ever block, useRestClientinstead — it is purpose-built for synchronous code and avoids pulling in the reactive stack.
retrieve vs exchange
retrieve is the concise default for “give me the body or throw on error.” exchange (exchangeToMono / exchange() on RestClient) hands you the full response so you can branch on status before reading the body.
| Style | Use when |
|---|---|
retrieve() | You want the body; errors should throw |
exchange() / exchangeToMono() | You need to inspect status/headers and decide |
// RestClient: inspect status explicitly
User user = client.get()
.uri("/users/{id}", id)
.exchange((request, response) -> {
if (response.getStatusCode().is2xxSuccessful()) {
return response.bodyTo(User.class);
}
throw new UpstreamException("Status " + response.getStatusCode());
});
Error handling
By default retrieve() throws on 4xx/5xx. Customize per status with onStatus.
// RestClient
public Optional<User> findUser(Long id) {
User user = client.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(status -> status.value() == 404, (req, res) -> {
throw new NotFoundException("User " + id);
})
.onStatus(HttpStatusCode::is5xxServerError, (req, res) -> {
throw new UpstreamException("Upstream error " + res.getStatusCode());
})
.body(User.class);
return Optional.ofNullable(user);
}
// WebClient
public Mono<User> getUser(Long id) {
return client.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
res -> Mono.error(new NotFoundException("User " + id)))
.bodyToMono(User.class);
}
Timeouts
Configure timeouts on the underlying connector. With RestClient over the JDK or Reactor Netty client:
import io.netty.channel.ChannelOption;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import reactor.netty.http.client.HttpClient;
import java.time.Duration;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
.responseTimeout(Duration.ofSeconds(5));
WebClient webClient = WebClient.builder()
.baseUrl("https://api.example.com")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
For reactive flows you can also bound the whole pipeline:
client.get().uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class)
.timeout(Duration.ofSeconds(5))
.retry(2);
Warning: Always set connect and response timeouts. Without them a slow upstream can exhaust your connection pool and stall the application.
Choosing a client
| Client | Model | Use for |
|---|---|---|
RestClient | Synchronous | New blocking MVC code (recommended default) |
WebClient | Reactive | WebFlux apps, high-concurrency fan-out |
RestTemplate | Synchronous | Legacy only — maintenance mode |
Pitfalls
- Mixing
.block()calls inside a reactive handler defeats non-blocking I/O and can deadlock — keep the chain reactive end-to-end. - Reusing one builder across services is fine, but call
.build()per distinct configuration; mutating a shared instance is error-prone. - Forgetting
onStatusmeans any non-2xx becomes a genericWebClientResponseExceptionyou must still translate.