skip to content

Status Handling, Timeouts & Retry

onStatus maps failures to exceptions, response timeouts are configured on the connector, and retryWhen with backoff plus a circuit breaker keeps a slow dependency from taking you down. Interviewers ask because a client with no timeout is an outage waiting to happen.

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

questions

5

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

level: juniorimportance: must knowfreq 70%

answer

  1. retrieve() = auto 4xx/5xx error
  2. WebClientResponseException + .NotFound/.BadRequest subclasses
  3. onStatus(predicate, resp -> Mono<Throwable>)
  4. exchangeToMono = no auto error, you release body
  5. handle with onErrorResume, not try/catch

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.

solid answer

~40 s

With retrieve(), WebClient automatically signals an error for any 4xx or 5xx response — a WebClientResponseException (with subclasses like NotFound, BadRequest) carrying the status code, headers and raw body. That error flows through the reactive pipeline, so you handle it with onErrorResume/onErrorMap, not try/catch. onStatus(HttpStatusCode::isError-style predicate, response -> Mono<Throwable>) lets you override this per status range: inspect the ClientResponse, read the body, and return a Mono of a domain-specific exception. If you use exchangeToMono() instead of retrieve(), there is NO automatic error signal — you inspect statusCode() yourself and must consume the body to avoid leaks. onStatus only applies to the retrieve() path.

code

java · 14 lines
java
Mono<Account> account = webClient.get()
    .uri("/accounts/{id}", id)
    .retrieve()
    // map 404 to a domain exception
    .onStatus(status -> status.value() == 404,
        response -> Mono.error(new AccountNotFoundException(id)))
    // map any remaining 5xx to a custom exception, decoding the error body
    .onStatus(HttpStatusCode::is5xxServerError,
        response -> response.bodyToMono(ApiError.class)
            .defaultIfEmpty(new ApiError("unknown"))
            .map(err -> new UpstreamServerException(err.message())))
    .bodyToMono(Account.class)
    // reactive error handling, NOT try/catch
    .onErrorResume(AccountNotFoundException.class, ex -> Mono.empty());

go deeper

for a junior

Know that retrieve() turns 4xx/5xx into WebClientResponseException and onStatus lets you remap a status range.

for a middle

Know the subclasses (.NotFound etc.), that handlers evaluate in order first-match, and that errors are reactive signals handled with onErrorResume/onErrorMap.

for a senior

Contrast retrieve() vs exchangeToMono(), body-release responsibilities, and decoding the error body inside onStatus.

for a principal

Design a consistent upstream-error taxonomy (mapping status ranges to domain exceptions) shared across clients, plus body-size limits and observability on failures.

**WebClient** is Spring WebFlux's non-blocking, reactive HTTP client (the reactive successor to RestTemplate). A call returns a **Mono** (0..1 values) or **Flux** (0..N values) — reactive publishers that emit either data or an error signal. **Two ways to get the response:** - `retrieve()` — the common, concise path. It has *built-in* error handling: any response with a **4xx or 5xx** status is automatically converted into an error signal on the stream. - `exchangeToMono()` / `exchangeToFlux()` — full control. There is **no** automatic error; you check `response.statusCode()` yourself and you are responsible for consuming/releasing the body (e.g. `releaseBody()`), or you leak the connection. **The default exception:** on the `retrieve()` path a non-2xx status raises **`WebClientResponseException`**. It extends `WebClientException` (a `RuntimeException`). It exposes `getStatusCode()`, `getStatusText()`, `getHeaders()`, `getResponseBodyAsString()` / `getResponseBodyAsByteArray()`. Spring provides **status-specific subclasses**: `WebClientResponseException.NotFound` (404), `.BadRequest` (400), `.Unauthorized` (401), `.Forbidden` (403), `.InternalServerError` (500), etc. You can `catch`/`onErrorResume` on the specific subclass. **Customizing with `onStatus`:** the signature is `onStatus(Predicate<HttpStatusCode> statusPredicate, Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction)`. When the predicate matches the response status, your function runs, receives the `ClientResponse` (from which you can decode the error body), and returns a **`Mono<Throwable>`** — the exception that will be signalled instead of the default. You can register multiple `onStatus` handlers; they are evaluated in order and the **first matching** one wins. A status not matched by any handler falls back to the default 4xx/5xx behavior. **Important gotcha:** inside the `onStatus` exception function you should consume the body (e.g. `response.bodyToMono(ErrorBody.class)`) — if you return an exception without reading the body, the response body may not be released. Also: `onStatus` handlers only fire on the `retrieve()` path; they are ignored if you use `exchangeToMono()`. **Where errors go:** because the error is a *reactive* signal, `try/catch` around the WebClient call does not catch it — the exception surfaces when the pipeline is subscribed. Handle it with operators: `onErrorMap` (translate one exception to another), `onErrorResume` (provide a fallback Mono/Flux), or `onErrorReturn` (a static fallback value). **When to use what:** use `retrieve()` + `onStatus` for standard REST calls where you want status→exception mapping. Use `exchangeToMono()` when you must decode the body differently per status, stream the body, or handle a status *without* treating it as an error (e.g. a 404 that means 'return empty').

  • Why can't you wrap a WebClient call in try/catch to handle a 500?
    The 500 becomes an error *signal* on the reactive stream, emitted asynchronously at subscription time — not a thrown exception at call-assembly time. You handle it with reactive operators like onErrorResume/onErrorMap, which run when the signal propagates.
  • What's the difference between retrieve() and exchangeToMono() for error handling?
    retrieve() auto-converts 4xx/5xx into WebClientResponseException and supports onStatus. exchangeToMono() gives raw ClientResponse with no auto error — you inspect statusCode() and must consume/release the body yourself, or you leak the connection.

saying these in an interview costs you the question

  • Thinking WebClient returns the response object for a 500 so you check a status field like RestTemplate — retrieve() signals an error instead.
  • Trying to catch WebClientResponseException with try/catch around the call.
  • Believing onStatus works with exchangeToMono() (it doesn't).
  • Returning an exception from onStatus without consuming the body, leaking the connection.

context

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

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