How do you customize error handling for specific HTTP statuses with retrieve(), and how does it compare to doing it via exchangeToMono?
answer
- onStatus(predicate, resp -> Mono<Throwable>)
- chain multiple, first match wins
- unmatched 4xx/5xx still default-throw
- createException() = default exception
- onErrorResume/onErrorMap/retryWhen downstream
basics
~20 sWith 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.
solid answer
~40 s`retrieve().onStatus(Predicate<HttpStatusCode>, Function<ClientResponse, Mono<? extends Throwable>>)` lets you intercept selected statuses and return a `Mono` of the exception you want, replacing the default `WebClientResponseException`. You can chain multiple `onStatus` calls (first matching wins) and read the error body inside the handler, e.g. decode an `ApiError` DTO and throw a domain exception. Anything not matched still falls through to default handling for 4xx/5xx. Then you combine it with reactive operators — `onErrorResume`, `onErrorMap`, `retryWhen` — downstream. Doing the same via `exchangeToMono` means branching on `statusCode()` yourself, decoding per branch, remembering to `releaseBody()` on discarded branches, and there is no default error mapping to lean on. So `retrieve().onStatus(...)` is the idiomatic choice for custom error mapping; `exchangeToMono` is reserved for when you also need headers/cookies or status-dependent success decoding.
code
java · 14 linesMono<Order> order = client.get()
.uri("/orders/{id}", id)
.retrieve()
.onStatus(status -> status.value() == 404,
resp -> Mono.error(new OrderNotFoundException(id)))
.onStatus(HttpStatusCode::is5xxServerError,
resp -> resp.bodyToMono(ApiError.class)
.defaultIfEmpty(ApiError.unknown())
.map(err -> new UpstreamServiceException(err.message())))
.bodyToMono(Order.class)
.retryWhen(Retry.backoff(3, Duration.ofMillis(200))
.filter(ex -> ex instanceof UpstreamServiceException))
.timeout(Duration.ofSeconds(5))
.onErrorResume(OrderNotFoundException.class, ex -> Mono.empty());go deeper
Know onStatus exists to customize error handling on retrieve().
Write onStatus mapping a status to a custom exception and decode the error body.
Chain multiple onStatus clauses, combine with retryWhen/onErrorResume/timeout, and explain the retrieve-vs-exchange trade-off for errors.
Design a resilient client policy: idempotent-only retries, backoff, timeout budgets, and consistent domain-exception translation across services.
## onStatus: targeted error mapping on retrieve() `retrieve()` returns a `ResponseSpec` with: ```java ResponseSpec onStatus(Predicate<HttpStatusCode> statusPredicate, Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) ``` - **`statusPredicate`** selects which statuses to handle, e.g. `HttpStatusCode::is5xxServerError`, `status -> status.value() == 429`, or `HttpStatusCode::isError`. - **`exceptionFunction`** receives the `ClientResponse` and returns a **`Mono<Throwable>`**. Whatever error it emits replaces the default. Inside it you can decode the error body (`response.bodyToMono(ApiError.class)`) and map it to a domain exception. Multiple `onStatus` clauses can be chained; they are evaluated in order and the **first matching predicate wins**. Statuses not matched by any clause still get the default: 4xx/5xx → `WebClientResponseException`. ```java Mono<Order> order = client.get().uri("/orders/{id}", id) .retrieve() .onStatus(s -> s.value() == 404, resp -> Mono.error(new OrderNotFoundException(id))) .onStatus(HttpStatusCode::is5xxServerError, resp -> resp.bodyToMono(ApiError.class) .map(e -> new UpstreamException(e.message()))) .bodyToMono(Order.class); ``` A key convenience: **`retrieve()` still auto-drains the body** even in your `onStatus` handler path, so there is no leak risk. If your handler decodes the body, that counts as consumption; if it doesn't, the framework releases it. ## Combining with reactive error operators The exception flows as an error signal, so downstream you use standard Reactor operators: - **`onErrorResume(ex -> fallback)`** — substitute a default/cached value. - **`onErrorMap(ex -> new DomainException(ex))`** — translate exceptions. - **`retryWhen(Retry.backoff(3, Duration.ofMillis(200)))`** — retry transient failures (often filtered to 5xx/timeouts). - **`timeout(Duration)`** — bound the call. ## Contrast with exchangeToMono The same custom mapping via `exchangeToMono` requires manual work: ```java .exchangeToMono(resp -> { if (resp.statusCode().is2xxSuccessful()) return resp.bodyToMono(Order.class); if (resp.statusCode().value() == 404) return resp.releaseBody().then(Mono.error(new OrderNotFoundException(id))); return resp.createException().flatMap(Mono::error); }); ``` You must branch, decode, and **release the body on every non-decoding branch**, and there's no default mapping. Use `exchangeToMono` only when you additionally need `headers()`/`cookies()` or must decode different success types per status. ## Gotchas - **`createException()`** on a `ClientResponse` builds the standard `WebClientResponseException` (reads and includes the body) — handy inside `exchangeToMono` to mimic the default. - `onStatus` predicates are checked only for the statuses you name; forgetting the default still applies is a common surprise (unmatched 4xx/5xx still throw). - Reading the error body twice (once in `onStatus`, again elsewhere) is not possible — the body is a one-shot stream. - Prefer filtering retries to idempotent methods and transient statuses; blindly retrying POSTs can double-submit.
- If none of your onStatus predicates match a 500 response, what does retrieve() do?It falls back to default handling and emits a WebClientResponseException (the 5xx subtype). onStatus only overrides the statuses whose predicates match; everything else keeps the default behavior.
- How would you retry only transient server errors with backoff?Map 5xx to a specific exception (or keep WebClientResponseException) then use retryWhen(Retry.backoff(n, duration).filter(predicate)) filtering on that exception/status. Restrict retries to idempotent requests to avoid duplicate side effects.
- Inside an onStatus handler, do you need to release the body manually?No. retrieve() still guarantees the body is drained/released. If your handler decodes it (bodyToMono), that consumes it; if not, the framework releases it. Manual releaseBody() is only needed with the exchangeTo* variants.
saying these in an interview costs you the question
- Believing onStatus overrides handling for ALL statuses, not just matched ones
- Thinking you must manually releaseBody inside retrieve().onStatus
- Retrying non-idempotent POSTs blindly with retryWhen
- Confusing onStatus (maps to exception) with onErrorResume (recovers from exception)