Mastering Spring WebFlux WebClient: Configuration, Code Samples, and Advanced Usage
Learn how to configure Spring WebFlux's WebClient with various HTTP client connectors, customize memory limits, handle responses, send request bodies and form data, apply filters, and use advanced exchange methods, all illustrated with concise Java code examples for reactive backend development.
Overview
Spring WebFlux includes a client for executing HTTP requests called WebClient. It provides a functional, fluent API based on Reactor that supports declarative composition of asynchronous logic without dealing with threads or concurrency. WebClient is fully non‑blocking, supports streaming, and uses the same codecs for encoding and decoding request and response bodies as the server side.
WebClient requires an HTTP client library to perform requests. Built‑in support includes:
Reactor Netty – https://github.com/reactor/reactor-netty
Jetty Reactive HttpClient – https://github.com/jetty-project/jetty-reactive-httpclient
Apache HttpComponents – https://hc.apache.org/index.html
Other implementations can be plugged in via ClientHttpConnector .
WebClient Configuration
The simplest way to create a WebClient is through one of the static factory methods:
WebClient.create()
WebClient.create(String baseUrl)
You can also use WebClient.builder() for more options:
uriBuilderFactory : custom UriBuilderFactory used as the base URL.
defaultUriVariables : default values used when expanding URI templates.
defaultHeader : headers applied to every request.
defaultCookie : cookies applied to every request.
defaultRequest : customizer for each request.
filter : client filter for each request.
exchangeStrategies : custom HTTP message readers/writers.
clientConnector : HTTP client library configuration.
Example 1:
<code>WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();</code>Once created, a WebClient instance is immutable. You can clone it and modify the copy:
Example 2:
<code>WebClient client1 = WebClient.builder()
.filter(filterA)
.filter(filterB)
.build();
// create a copy
WebClient client2 = client1.mutate()
.filter(filterC)
.filter(filterD)
.build();</code>Maximum Memory Configuration
To avoid out‑of‑memory issues, codecs limit the amount of data cached in memory. By default the limit is 256KB . If this limit is exceeded you get a org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer .
Modify the default maximum memory size:
<code>WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();</code>Reactor Netty
Network request HTTP client.
Custom Reactor Netty settings, including timeouts:
<code>HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.doOnConnected(con -> {
con.addHandlerFirst(new ReadTimeoutHandler(2, TimeUnit.SECONDS));
con.addHandlerLast(new WriteTimeoutHandler(10));
});
ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
WebClient.Builder builder = WebClient.builder().clientConnector(connector);
// response timeout configuration
HttpClient httpClient = HttpClient.create().responseTimeout(Duration.ofSeconds(2));</code>Getting Response Data
<code>WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
</code>Only retrieve the response body:
<code>Mono<Person> result = client.get()
.uri("/persons/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
</code>By default, 4xx or 5xx responses trigger a WebClientResponseException . To customize error handling use onStatus :
<code>Mono<Person> result = client.get()
.uri("/persons/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
</code>Exchange Operations
exchangeToMono() and exchangeToFlux() are useful for advanced scenarios that need more control, such as handling responses differently based on status:
<code>@GetMapping("/removeInvoke3")
public Mono<R> remoteInvoke3() {
return wc.get()
.uri("http://localhost:9000/users/get?id={id}", new Random().nextInt(1000000))
.exchangeToMono(clientResponse -> {
if (clientResponse.statusCode().equals(HttpStatus.OK)) {
return clientResponse.bodyToMono(Users.class);
} else {
return clientResponse.createException().flatMap(Mono::error);
}
})
.log()
.flatMap(user -> Mono.just(R.success(user)))
.retry(3) // retry count
.onErrorResume(ex -> Mono.just(R.failure(ex.getMessage())));
}
</code>Request Body
The request body can be encoded from any asynchronous type handled by ReactiveAdapterRegistry , such as a Mono :
<code>Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
</code>If you have an actual value, use bodyValue :
<code>Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
</code>Form Data
To send form data, provide a MultiValueMap as the body. The content type is automatically set to application/x-www-form-urlencoded by FormHttpMessageWriter :
<code>MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
</code>Convenient Method
<code>import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.bodyToMono(Void.class);
</code>Filters
You can register client filters ( ExchangeFilterFunction ) with WebClient to intercept and modify requests:
<code>WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
</code>Done!!
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.