skip to content

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

level: middleimportance: must knowfreq 82%

answer

  1. retrieve = body only + auto-error + auto-release
  2. exchangeToMono = full ClientResponse, you decode
  3. exchange variants: YOU must consume body or leak
  4. exchange() deprecated -> exchangeToMono
  5. prefer retrieve, escalate for status/headers

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.

solid answer

~40 s

`retrieve()` returns a `ResponseSpec` exposing only the body, with default status handling that converts 4xx/5xx into a `WebClientResponseException`. `exchangeToMono(Function<ClientResponse, Mono<T>>)` (and `exchangeToFlux` for streams) hands you the whole `ClientResponse` — status code, headers, cookies, raw body — and *you* return a `Mono<T>` describing how to turn it into a result. That gives full control: branch on status, read a header before decoding, decode different types per status, or map an error body to a domain exception without an exception being thrown. The crucial rule with the exchange variants is that **you must always consume the response body** (call `bodyToMono`, `bodyToFlux`, or `releaseBody()`), otherwise the connection leaks and is not returned to the pool. `retrieve()` handles that for you. Prefer `retrieve()`; reach for `exchangeToMono` only when you genuinely need response metadata.

code

java · 19 lines
java
// retrieve(): common case, body only, auto error + release
Mono<Product> viaRetrieve = client.get().uri("/products/{id}", id)
    .retrieve()
    .bodyToMono(Product.class);

// exchangeToMono(): full control over status / headers / body
Mono<Product> viaExchange = client.get().uri("/products/{id}", id)
    .exchangeToMono(response -> {
        HttpStatusCode status = response.statusCode();
        if (status.is2xxSuccessful()) {
            return response.bodyToMono(Product.class);
        } else if (status.value() == 404) {
            return response.releaseBody()          // MUST consume body
                           .then(Mono.empty());    // 404 -> empty, not error
        } else {
            return response.createException()       // build WebClientResponseException
                           .flatMap(Mono::error);
        }
    });

go deeper

for a junior

Know retrieve() = body shortcut, exchangeToMono = full response.

for a middle

Explain default-vs-manual error handling and the mandatory body-consumption rule.

for a senior

Articulate concrete cases (per-status decoding, header reads) and why exchange() was deprecated.

for a principal

Reason about connection-pool exhaustion from leaked bodies and set team guidance defaulting to retrieve().

## Two ways to consume a response After building a request, you finish it with either `retrieve()` or an `exchangeTo*` operator. ### retrieve() — the high-level shortcut - Returns a **`WebClient.ResponseSpec`**. - Exposes **only the body** via `bodyToMono` / `bodyToFlux` / `toEntity` / `toBodilessEntity`. - Applies **default status handling**: 4xx/5xx → `WebClientResponseException` error signal. - **Automatically drains/releases the body**, so you never leak connections. - Customizable via **`.onStatus(predicate, handler)`** before extracting the body. ### exchangeToMono / exchangeToFlux — full control - Signature: `exchangeToMono(Function<ClientResponse, Mono<T>>)` and `exchangeToFlux(Function<ClientResponse, Flux<T>>)`. - The lambda receives a **`ClientResponse`**, which exposes `statusCode()`, `headers()`, `cookies()`, and body decoders `bodyToMono`/`bodyToFlux`. - **No default error handling** — a 500 is just a `ClientResponse` with that status; nothing throws unless you make it. - **You are responsible for consuming the body.** If a branch returns without reading it, you must call `response.releaseBody()` (or `bodyToMono(Void.class)`) to avoid a **connection leak**. ## Why exchangeToMono exists — use cases 1. **Status-dependent decoding:** decode `Product` on 200 but an `ApiError` on 422, mapping the latter to a custom exception. 2. **Read headers/cookies before/with the body:** e.g. pagination headers, `ETag`, or a rate-limit header. 3. **Treat some 4xx as non-errors:** e.g. map 404 to `Mono.empty()` instead of an exception. 4. **Conditional body handling:** skip decoding for `204 No Content`. ## The deprecated exchange() The older **`exchange()`** returned `Mono<ClientResponse>` directly and was **deprecated** precisely because it made body-consumption too easy to forget, causing leaks. `exchangeToMono`/`exchangeToFlux` replaced it: the functional form keeps you inside a scope where the framework can guarantee the body is released if you don't. Do not mention `exchange()` as current API. ## Decision guide - **Default to `retrieve()`** — it is safer (auto body release) and less code. - Use **`exchangeToMono`/`exchangeToFlux`** only when you need the status code, headers, or per-status decoding logic that `retrieve()` + `onStatus` can't cleanly express. ## Gotchas - Forgetting to consume the body in an `exchangeTo*` branch → connection pool exhaustion under load. - Assuming `exchangeToMono` throws on 5xx — it does not; only `retrieve()` (or your own code) does. - `retrieve().onStatus(...)` covers most 'custom error' needs without dropping to `exchangeToMono`.

  • What happens if, inside exchangeToMono, a branch returns Mono.empty() without reading the body?
    The response body is never consumed, so the underlying connection is not released back to the pool — a connection leak. Under load the pool exhausts and requests hang/time out. You must call response.releaseBody() (or bodyToMono) even when discarding the body.
  • Can retrieve() with onStatus replace most uses of exchangeToMono?
    Yes for custom error mapping — onStatus(predicate, handler) lets you map specific statuses to exceptions while retrieve() still auto-releases the body. You only need exchangeToMono when you must inspect headers/cookies or decode different body types per status.
  • Why was the old exchange() method deprecated?
    It returned Mono<ClientResponse> with no guardrails, so developers routinely forgot to consume the body, leaking connections. exchangeToMono/exchangeToFlux keep body handling inside a framework-managed function that can release it for you.

saying these in an interview costs you the question

  • Saying exchangeToMono auto-throws on 4xx/5xx like retrieve()
  • Not knowing you must consume the body with exchange variants
  • Recommending the deprecated exchange() method
  • Claiming retrieve() gives access to response headers/status (it needs toEntity/exchange for full metadata)

context