skip to content

Reactive WebClient

The non-blocking HTTP client: retrieve versus exchange, streaming bodies, filter functions for cross-cutting concerns, and status handling with timeouts and retries. WebClient shows up in interviews even for MVC apps, since it is the recommended client for fan-out calls.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

19

When sending a request body with WebClient, what is the difference between bodyValue(obj) and body(publisher, Class)? When would you use each?

level: juniorimportance: must knowfreq 70%

answer

  1. bodyValue = ready object
  2. body(publisher,Class) = Mono/Flux
  3. bodyValue rejects reactive types → IllegalArgumentException
  4. fromValue vs fromPublisher under the hood
  5. Flux body = streamed upload

basics

~20 s

bodyValue takes a single already-available object and serializes it. body(publisher, Class) takes a reactive stream (Mono/Flux) that WebClient subscribes to and serializes element by element. Use bodyValue for one plain object, body(...) for reactive/streaming data.

solid answer

~40 s

bodyValue(Object) is for a concrete, materialized value you already hold in memory — WebClient serializes it (e.g. via Jackson) into the request body. It rejects reactive types: passing a Mono/Flux to bodyValue throws IllegalArgumentException. body(Publisher, Class) (or body(Publisher, ParameterizedTypeReference)) is for a reactive producer: WebClient subscribes to the Mono/Flux and writes each emitted element, enabling true streaming without collecting everything first. So bodyValue(dto) for a single POJO; body(fluxOfDtos, Dto.class) when the payload comes from a stream (DB cursor, another WebClient call) you want to pass through lazily. Under the hood bodyValue is essentially BodyInserters.fromValue and body(publisher,...) is BodyInserters.fromPublisher, but the fluent methods are preferred.

code

java · 14 lines
java
// Single, already-available object
Mono<Void> a = webClient.post().uri("/users")
    .bodyValue(new UserDto("neo"))       // serialized as-is
    .retrieve().bodyToMono(Void.class);

// Reactive/streaming producer (Flux) -> streamed upload
Flux<UserDto> users = userRepository.findAll(); // e.g. R2DBC
Mono<Void> b = webClient.post().uri("/users/bulk")
    .contentType(MediaType.APPLICATION_NDJSON) // stream as NDJSON
    .body(users, UserDto.class)          // subscribes to the Flux
    .retrieve().bodyToMono(Void.class);

// This THROWS IllegalArgumentException:
// .bodyValue(Mono.just(new UserDto("trinity")))

go deeper

for a junior

Should know bodyValue = plain object, body(publisher,Class) = reactive stream, and that mixing them up throws.

for a middle

Should mention BodyInserters.fromValue/fromPublisher and that Flux enables streaming upload.

for a senior

Adds media-type implications (JSON array vs NDJSON) and laziness/backpressure of body(Flux).

for a principal

Frames it as encoder/HttpMessageWriter strategy selection and when streaming upload actually matters for memory/latency.

**Context.** `WebClient` is Spring WebFlux's non-blocking, reactive HTTP client (the reactive analog of `RestTemplate`). When you build a request you eventually specify the request body. There are two fluent ways on `RequestBodySpec`: - **`bodyValue(Object body)`** — you pass an already-computed value (a POJO, a `String`, `byte[]`, a `Map`, etc.). WebClient hands it to an `HttpMessageWriter` (typically `Jackson2JsonEncoder` for JSON) which serializes it into the body. It is *synchronous input*: the object must already exist in memory. Crucially, `bodyValue` **rejects reactive types** — if you pass a `Mono` or `Flux`, it throws `IllegalArgumentException` at runtime telling you to use `body(...)` instead. - **`body(Publisher<T> publisher, Class<T> elementClass)`** (and overloads taking `ParameterizedTypeReference<T>`) — you pass a reactive **`Publisher`** (`Mono<T>` or `Flux<T>`). WebClient will **subscribe** to it when the request is executed and encode each emitted element into the outbound body. A `Mono<T>` behaves like a single value; a `Flux<T>` **streams** multiple elements, so the body is written incrementally as elements arrive — you never need to collect the whole thing into memory. **Relationship to `BodyInserters`.** A `BodyInserter` is the lower-level strategy object that actually writes the body to the `ClientHttpRequest`. The fluent methods are thin sugar: - `bodyValue(x)` ≈ `body(BodyInserters.fromValue(x))` - `body(publisher, Class)` ≈ `body(BodyInserters.fromPublisher(publisher, Class))` You rarely call `BodyInserters` directly today; prefer `bodyValue` / `body(publisher, ...)`. (`BodyInserters.fromValue` replaced the deprecated `syncBody`/`fromObject`.) **When to use each.** - Single materialized object → `bodyValue(dto)`. - The data is produced reactively (a `Flux` from R2DBC, another WebClient response, a generator) and you want lazy, backpressured, streaming upload → `body(flux, Dto.class)`. - A single async value you don't want to `.block()` on → `body(mono, Dto.class)` (or `bodyValue(mono.block())` only if blocking is acceptable, which it usually is not in reactive code). **Gotchas.** - Passing a reactive type to `bodyValue` = `IllegalArgumentException`. This is the most common mistake. - `body(...)` needs the element `Class`/`ParameterizedTypeReference` because generic type is erased and the encoder must know what it's writing. - The `Content-Type` still matters: for a `Flux` streamed as JSON you typically get a JSON array unless you set a streaming media type like `application/x-ndjson`. - Nothing is sent until the returned response `Mono`/`Flux` is subscribed — WebClient is lazy end to end.

  • What happens if you pass a Mono to bodyValue()?
    It throws IllegalArgumentException. bodyValue expects an already-resolved value; reactive publishers must go through body(publisher, Class) so WebClient can subscribe to them.
  • What replaced the old syncBody() method?
    bodyValue() (and BodyInserters.fromValue()). syncBody and BodyInserters.fromObject were deprecated/removed in favor of bodyValue/fromValue, which also handle empty publishers more consistently.

saying these in an interview costs you the question

  • Thinking bodyValue can accept a Mono/Flux
  • Believing body(publisher) collects the whole stream into memory before sending
  • Confusing bodyValue with bodyToMono (request vs response)
  • Saying syncBody is the current method

context

open as a page

What is an ExchangeFilterFunction in Spring WebClient, and how do you register one?

level: juniorimportance: must knowfreq 60%

basics

~10 s

It is an interceptor for WebClient calls. It receives each outgoing request plus the next step in the chain, and can modify the request or response. You register it with WebClient.builder().filter(...).

open as a page

How does WebClient handle non-2xx HTTP responses by default, and how do you customize that with onStatus?

level: juniorimportance: must knowfreq 70%

basics

~10 s

By default, when you call retrieve(), WebClient turns 4xx/5xx responses into a WebClientResponseException error on the reactive stream. onStatus(predicate, handler) lets you intercept a status range and map it to your own exception.

open as a page

How does retrieve() work with bodyToMono and bodyToFlux to decode a response?

level: juniorimportance: must knowfreq 70%

basics

~10 s

retrieve() triggers the request and gives you the response body directly. bodyToMono(Type.class) decodes a single object; bodyToFlux(Type.class) decodes a stream/collection into many elements. retrieve() auto-throws WebClientResponseException on 4xx/5xx.

open as a page

What is Spring's WebClient and why is it the recommended replacement for RestTemplate?

level: juniorimportance: must knowfreq 78%

basics

~20 s

WebClient is Spring's non-blocking, reactive HTTP client. Unlike RestTemplate, which blocks a thread per request, WebClient uses async I/O and returns Mono/Flux, so a few threads handle many concurrent calls. RestTemplate is in maintenance mode.

open as a page

How do you configure a response timeout for WebClient, and how does it differ from other timeout types?

level: middleimportance: must knowfreq 65%

basics

~10 s

Configure a Reactor Netty HttpClient with responseTimeout(Duration), wrap it in a ReactorClientHttpConnector, and pass it to WebClient.builder().clientConnector(...). responseTimeout limits the time waiting for the full response after the request is sent.

open as a page

What is the difference between retrieve() and exchangeToMono()/exchangeToFlux(), and when would you use each?

level: middleimportance: must knowfreq 82%

basics

~20 s

retrieve() is the shortcut that gives you just the body and auto-errors on 4xx/5xx. exchangeToMono/exchangeToFlux give you the full ClientResponse — status, headers, cookies — so you decide how to decode based on those. Use retrieve() for the common case; exchange when you need full control.

open as a page

How do you consume a streaming HTTP response with WebClient as a Flux<T>, and how does bodyToFlux differ from bodyToMono in backpressure and memory behavior?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Call retrieve() (or exchangeToFlux) then bodyToFlux(T.class). It decodes the response body incrementally into a stream of T with backpressure, so you process elements as they arrive without buffering the whole response. bodyToMono decodes into a single value/object, buffering that one payload.

open as a page

How do you add retries with exponential backoff to a WebClient call, and how do you retry only the right failures?

level: seniorimportance: must knowfreq 60%

basics

~10 s

Use retryWhen(Retry.backoff(maxAttempts, minBackoff)) on the Mono/Flux. Add a .filter(...) predicate so you only retry transient failures (like 5xx or timeouts), not 4xx client errors, and set .jitter(...) and .maxBackoff(...).

open as a page

How do you stream a large, incrementally-produced dataset as a WebClient request body without buffering it all in memory, and what media type makes it a true stream?

level: middleimportance: should knowfreq 45%

basics

~20 s

Pass a Flux to body(flux, Type.class) so WebClient writes elements as they are emitted instead of collecting them. Use a streaming media type like application/x-ndjson (or text/event-stream) so each element is flushed separately rather than buffered into one JSON array.

open as a page

What do ExchangeFilterFunction.basicAuthentication, ofRequestProcessor, and ofResponseProcessor give you?

level: middleimportance: should knowfreq 45%

basics

~10 s

They are ready-made factory methods. basicAuthentication(user, password) adds an HTTP Basic auth header. ofRequestProcessor lets you transform just the request; ofResponseProcessor lets you transform just the response — without writing the full filter method.

open as a page

In what order do WebClient exchange filters execute, and how do you control that order?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Filters run in the order you add them on the builder. The first filter added sees the request first and the response last (it wraps the others, like nested layers). Use .filters(list -> ...) to reorder or insert.

open as a page

How would you implement retry and request/response logging as exchange filters, and what pitfalls arise?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Write a filter that calls next.exchange(request) and adds .retryWhen(...) for retries, or logs request details before/after. Main pitfalls: reading the body inside a filter consumes it, and retrying isn't safe when the request body can't be replayed.

open as a page

How do you add a circuit breaker around a WebClient call in a reactive Spring app, and why?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Spring doesn't have a built-in circuit breaker; use Resilience4j (via spring-cloud-circuitbreaker or its reactor operators). Wrap the WebClient Mono with a CircuitBreakerOperator or transformDeferred(cb) so that after too many failures it 'opens' and fast-fails instead of calling the dying upstream.

open as a page

How do you customize error handling for specific HTTP statuses with retrieve(), and how does it compare to doing it via exchangeToMono?

level: seniorimportance: should knowfreq 60%

basics

~20 s

With retrieve(), chain .onStatus(statusPredicate, response -> Mono<Throwable>) before bodyToMono to map chosen statuses to custom exceptions. retrieve() still auto-releases the body. exchangeToMono can do the same but you must consume the body yourself and decode manually.

open as a page

Explain DataBuffer-level body transfer in WebFlux: what DataBuffer/DataBufferUtils are, how to stream a response body as Flux<DataBuffer> to disk, and the memory-management rules you must follow.

level: principalimportance: should knowfreq 30%

basics

~20 s

DataBuffer is WebFlux's abstraction over a chunk of bytes (often a pooled Netty ByteBuf). You can get the raw body as Flux<DataBuffer> and write it out with DataBufferUtils (e.g. write to a channel). Because buffers are pooled/reference-counted, you must release each one after use or you leak memory.

open as a page

How do you compose timeouts, retries, and a circuit breaker on a WebClient call correctly, and what ordering and idempotency concerns govern the design?

level: principalimportance: should knowfreq 35%

basics

~20 s

Layer them: a per-attempt timeout inside bounded, filtered, jittered retries; a circuit breaker outside the retries so it sees final failures; a fallback outermost. Only retry idempotent calls, keep the total time budget under upstream deadlines, and scope one breaker per dependency.

open as a page

You are migrating a blocking Spring MVC service from RestTemplate to WebClient. What are the key pitfalls and how do you avoid them?

level: principalimportance: should knowfreq 48%

basics

~20 s

Reuse one WebClient (not per-request), map RestTemplate calls to get()/post()+retrieve()+bodyToMono, translate error handling to onStatus, set explicit timeouts and connection-pool limits, and if you must stay blocking, call .block() carefully — never on a reactive event-loop thread.

open as a page

Design a resilient, reusable exchange-filter chain (tracing, token refresh, retry, logging) for a shared WebClient. What ordering and correctness concerns drive it?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Put tracing outermost, then retry, then auth/token-refresh, then logging closest to the call — so each retry attempt re-runs auth and logging under one trace span. Keep filters stateless, non-blocking, and configure them once on a shared WebClient bean.

open as a page