What is an ExchangeFilterFunction in Spring WebClient, and how do you register one?
answer
- filter(ClientRequest, ExchangeFunction) -> Mono<ClientResponse>
- next.exchange(request) continues chain
- ClientRequest is immutable -> from().build()
- register via builder().filter(...)
- non-blocking, shared thread-safe client
basics
~10 sIt 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(...).
solid answer
~40 sExchangeFilterFunction is WebClient's equivalent of a servlet filter or interceptor: a functional interface with one method, filter(ClientRequest request, ExchangeFunction next), returning Mono<ClientResponse>. You inspect or rewrite the request, then call next.exchange(request) to continue the chain, and can also transform the returned response. It is the standard place for cross-cutting concerns shared by every call: adding auth headers, logging, correlation IDs, metrics, and retries. You attach it once on the builder with WebClient.builder().filter(myFilter).build(), so every request made by that client passes through it. Because WebClient is reactive, everything is expressed as Monos, and the filter must be non-blocking. A single configured WebClient (and its filters) is thread-safe and meant to be shared as a bean.
code
java · 12 linesExchangeFilterFunction correlationId = (request, next) -> {
// ClientRequest is immutable: build a modified copy
ClientRequest modified = ClientRequest.from(request)
.header("X-Correlation-Id", UUID.randomUUID().toString())
.build();
return next.exchange(modified); // continue the chain
};
WebClient client = WebClient.builder()
.baseUrl("https://api.example.com")
.filter(correlationId)
.build();go deeper
Know it's an interceptor for WebClient, registered via builder().filter(), with signature filter(ClientRequest, ExchangeFunction).
Know ClientRequest immutability, that next.exchange continues the chain, and that filters must be non-blocking.
Contrast with RestTemplate interceptors and server WebFilter; understand body-consumption pitfalls.
Reason about filters as a reusable platform concern shared across many clients via a common builder configuration.
## What it is `ExchangeFilterFunction` is the interception mechanism for Spring's reactive `WebClient` (the WebFlux replacement for `RestTemplate`). It plays the same role that a `ClientHttpRequestInterceptor` plays for `RestTemplate`, or that a servlet `Filter` plays on the server side: it wraps every outgoing HTTP exchange so you can run shared logic in one place instead of copy-pasting it into every call site. It is a **functional interface** with a single abstract method: ```java Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next); ``` - **`ClientRequest`** is an *immutable* representation of the outgoing request (URL, method, headers, cookies, body). To change it you call `ClientRequest.from(request).header(...).build()` to produce a new one. - **`ExchangeFunction`** is "the rest of the chain": calling `next.exchange(newRequest)` performs the actual HTTP call (or hands off to the next filter) and returns `Mono<ClientResponse>`. - **`ClientResponse`** is the response; you can inspect status/headers and optionally transform it before returning. Because the method returns a `Mono`, the whole thing is **non-blocking / reactive** — you must not block a thread inside a filter (no `.block()`, no blocking I/O), or you can starve the small event-loop thread pool. ## How to register it Attach filters on the builder: ```java WebClient client = WebClient.builder() .baseUrl("https://api.example.com") .filter(loggingFilter()) // add one .filter(authFilter()) // add another .build(); ``` Each `.filter(...)` **appends** to an ordered list. There is also `.filters(Consumer<List<ExchangeFilterFunction>>)` which hands you the *mutable list* so you can insert at a position, reorder, or clear. ## Why use it Cross-cutting concerns that should apply to **every** request from a client: - Auth headers (bearer token, basic auth) - Logging / correlation-id propagation - Metrics and tracing - Retry / error normalization Defining them as filters keeps call sites clean and guarantees consistency. ## Gotchas - `ClientRequest` is immutable — you must build a copy to mutate. - Filters run on reactive threads; never block. - Reading a request or response **body** inside a filter consumes/buffers it, which can break the actual call if not handled carefully (see the logging question). - Configure filters once on a shared `WebClient` bean; don't rebuild a `WebClient` per request.
- Why must a filter avoid calling .block() inside filter(...)?WebClient runs on a small pool of event-loop threads. Blocking one of them stalls all requests scheduled on it and can deadlock. Everything must stay reactive, chaining Monos instead of blocking.
- How is ExchangeFilterFunction different from RestTemplate's ClientHttpRequestInterceptor?Same conceptual role (per-request interception) but reactive/non-blocking: it returns Mono<ClientResponse> and composes via ExchangeFunction, whereas the interceptor is synchronous and returns a ClientHttpResponse directly.
saying these in an interview costs you the question
- Thinking you can mutate ClientRequest directly instead of building a copy via ClientRequest.from(...)
- Blocking inside the filter (e.g., calling .block() or doing synchronous I/O)
- Confusing it with server-side WebFilter, which intercepts incoming requests, not outgoing WebClient calls