Backend Development 9 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring WebFlux WebClient: Configuration, Code Samples, and Advanced Usage

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!!

Javabackend-developmentreactive programmingWebClientSpring WebFlux
Spring Full-Stack Practical Cases
Written by

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.

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.